add lock
subcommand; add --local flag to rm and exec
All checks were successful
CI / build (push) Successful in 23s
All checks were successful
CI / build (push) Successful in 23s
This commit is contained in:
parent
dac1a06e1a
commit
1922138133
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/build
|
||||
/envvault
|
||||
.aider*
|
||||
.envvault.json.enc
|
||||
|
@ -6,16 +6,18 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"golang.org/x/term"
|
||||
"slices"
|
||||
|
||||
"git.jrop.me/jonathan/envvault/internal"
|
||||
"git.jrop.me/jonathan/envvault/internal/config"
|
||||
@ -24,9 +26,14 @@ import (
|
||||
|
||||
var cachedPassword []byte
|
||||
|
||||
const (
|
||||
EnvStoreCurrentVersion = 1
|
||||
)
|
||||
|
||||
var (
|
||||
keyFilePath string
|
||||
dbFilePath string
|
||||
keyFilePath string
|
||||
dbFilePath string
|
||||
localDbFilePath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -36,10 +43,79 @@ func init() {
|
||||
}
|
||||
keyFilePath = filepath.Join(homeDir, ".local/cache/envvault/key")
|
||||
dbFilePath = filepath.Join(homeDir, ".local/cache/envvault/db.json.enc")
|
||||
localDbFilePath = ".envvault.json.enc"
|
||||
}
|
||||
|
||||
type EnvStore struct {
|
||||
Vars map[string]string `json:"vars"`
|
||||
Vars map[string]string `json:"vars"`
|
||||
Tags map[string][]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// GetTags returns all tags for a given variable name
|
||||
func (s *EnvStore) GetTags(varName string) []string {
|
||||
if s.Tags == nil {
|
||||
return []string{}
|
||||
}
|
||||
return s.Tags[varName]
|
||||
}
|
||||
|
||||
// SetTags sets the tags for a given variable name
|
||||
func (s *EnvStore) SetTags(varName string, tags []string) {
|
||||
if s.Tags == nil {
|
||||
s.Tags = make(map[string][]string)
|
||||
}
|
||||
|
||||
// Create a union of existing and new tags
|
||||
existingTags := s.Tags[varName]
|
||||
unionTags := make([]string, 0, len(existingTags)+len(tags))
|
||||
|
||||
// Add existing tags to the union
|
||||
for _, tag := range existingTags {
|
||||
unionTags = append(unionTags, tag)
|
||||
}
|
||||
|
||||
// Add new tags if they don't already exist
|
||||
for _, tag := range tags {
|
||||
if !slices.Contains(unionTags, tag) {
|
||||
unionTags = append(unionTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
s.Tags[varName] = unionTags
|
||||
}
|
||||
|
||||
// RemoveTags removes the specified tags from a variable
|
||||
func (s *EnvStore) RemoveTags(varName string, tagsToRemove []string) {
|
||||
if s.Tags == nil || len(s.Tags[varName]) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
existingTags := s.Tags[varName]
|
||||
remainingTags := make([]string, 0, len(existingTags))
|
||||
|
||||
// Keep only tags that are not in tagsToRemove
|
||||
for _, tag := range existingTags {
|
||||
if !slices.Contains(tagsToRemove, tag) {
|
||||
remainingTags = append(remainingTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
s.Tags[varName] = remainingTags
|
||||
}
|
||||
|
||||
// HasAnyTag returns true if the variable has any of the specified tags
|
||||
func (s *EnvStore) HasAnyTag(varName string, tags []string) bool {
|
||||
if len(tags) == 0 {
|
||||
return true // No tags specified, include all variables
|
||||
}
|
||||
|
||||
varTags := s.GetTags(varName)
|
||||
for _, tag := range tags {
|
||||
if slices.Contains(varTags, tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type CLI struct {
|
||||
@ -47,31 +123,47 @@ type CLI struct {
|
||||
List ListCmd `cmd:"" help:"List all environment variables."`
|
||||
Add AddCmd `cmd:"" help:"Add an environment variable."`
|
||||
Rm RmCmd `cmd:"" help:"Remove an environment variable."`
|
||||
Exec ExecCmd `cmd:"" help:"Execute a command with environment variables." alias:"x"`
|
||||
Tag TagCmd `cmd:"" help:"Add or remove tags from an environment variable."`
|
||||
Exec ExecCmd `cmd:"" help:"Execute a command with environment variables." alias:"x"`
|
||||
Rekey RekeyCmd `cmd:"" help:"Change the master password."`
|
||||
Daemon DaemonCmd `cmd:"" help:"Start the password caching daemon."`
|
||||
Lock LockCmd `cmd:"" help:"Lock the vault by stopping the password caching daemon."`
|
||||
}
|
||||
|
||||
type InitCmd struct{}
|
||||
type ListCmd struct {
|
||||
Values bool `short:"v" help:"Show values of environment variables." default:"false"`
|
||||
Values bool `short:"v" help:"Show values of environment variables." default:"false"`
|
||||
Local bool `short:"l" help:"List only from local vault." default:"false"`
|
||||
Tags []string `short:"t" help:"Filter by tags (can be specified multiple times)."`
|
||||
}
|
||||
type AddCmd struct {
|
||||
Name string `arg:"" help:"Name of the environment variable."`
|
||||
Value string `arg:"" optional:"" help:"Value of the environment variable."`
|
||||
Value string `arg:"" help:"Value of the environment variable." optional:""`
|
||||
Local bool ` help:"Add to local vault instead of global." short:"l" default:"false"`
|
||||
}
|
||||
type RmCmd struct {
|
||||
Name string `arg:"" help:"Name of the environment variable to remove."`
|
||||
Name string `arg:"" help:"Name of the environment variable to remove."`
|
||||
Local bool ` help:"Remove from local vault only." short:"l" default:"false"`
|
||||
}
|
||||
type TagCmd struct {
|
||||
Name string `arg:"" help:"Name of the environment variable."`
|
||||
Tags []string `arg:"" help:"Tags to add or remove." optional:""`
|
||||
Remove bool ` help:"Remove tags instead of adding them." short:"r" default:"false"`
|
||||
Local bool ` help:"Operate on local vault only." short:"l" default:"false"`
|
||||
List bool ` help:"List tags for the variable." short:"L" default:"false"`
|
||||
}
|
||||
type ExecCmd struct {
|
||||
Env []string `short:"e" help:"Environment variables to set."`
|
||||
Cmd string `arg:"" help:"Command to execute." passthrough:"all"`
|
||||
Args []string `arg:"" optional:"" help:"Arguments for the command."`
|
||||
Env []string `short:"e" help:"Environment variables to set."`
|
||||
Local bool `short:"l" help:"Use only local vault." default:"false"`
|
||||
Tags []string `short:"t" help:"Filter by tags (can be specified multiple times)."`
|
||||
Cmd string ` help:"Command to execute." arg:"" passthrough:"all"`
|
||||
Args []string ` help:"Arguments for the command." arg:"" optional:""`
|
||||
}
|
||||
type RekeyCmd struct{}
|
||||
type DaemonCmd struct {
|
||||
Timeout int `help:"Password cache timeout in minutes." default:"5"`
|
||||
}
|
||||
type LockCmd struct{}
|
||||
|
||||
func main() {
|
||||
internal.Log.Println("Starting envvault")
|
||||
@ -86,11 +178,11 @@ func main() {
|
||||
case "init":
|
||||
subcommandInitVault()
|
||||
case "add <name>":
|
||||
subcommandAddEnvVar(cli.Add.Name, cli.Add.Value)
|
||||
subcommandAddEnvVar(cli.Add)
|
||||
case "add <name> <value>":
|
||||
subcommandAddEnvVar(cli.Add.Name, cli.Add.Value)
|
||||
subcommandAddEnvVar(cli.Add)
|
||||
case "rm <name>":
|
||||
subcommandRmEnvVar(cli.Rm.Name)
|
||||
subcommandRmEnvVar(cli.Rm.Name, cli.Rm.Local)
|
||||
case "list":
|
||||
subcommandListEnvVars(cli.List)
|
||||
case "exec <cmd>":
|
||||
@ -101,6 +193,12 @@ func main() {
|
||||
subcommandRekeyVault()
|
||||
case "daemon":
|
||||
subcommandStartDaemon(cli.Daemon.Timeout)
|
||||
case "lock":
|
||||
subcommandLockVault()
|
||||
case "tag <name>":
|
||||
subcommandTagEnvVar(cli.Tag)
|
||||
case "tag <name> <tags>":
|
||||
subcommandTagEnvVar(cli.Tag)
|
||||
default:
|
||||
internal.Log.Printf("Unknown command: %s", command)
|
||||
log.Fatal("Unknown command")
|
||||
@ -165,7 +263,7 @@ func subcommandInitVault() { // {{{
|
||||
// Initialize the db file
|
||||
internal.Log.Println("Initializing empty environment store")
|
||||
emptyStore := &EnvStore{Vars: make(map[string]string)}
|
||||
saveEnvStore(emptyStore)
|
||||
saveGlobalEnvStore(emptyStore)
|
||||
|
||||
internal.Log.Println("Vault initialized successfully")
|
||||
fmt.Fprintln(os.Stderr, "Vault initialized successfully.")
|
||||
@ -177,47 +275,205 @@ func subcommandInitVault() { // {{{
|
||||
|
||||
// }}}
|
||||
|
||||
func subcommandAddEnvVar(name, value string) { // {{{
|
||||
store := loadEnvStore()
|
||||
if value == "" {
|
||||
fmt.Fprintf(os.Stderr, "Enter value for %s: ", name)
|
||||
func subcommandAddEnvVar(cmd AddCmd) { // {{{
|
||||
var store *EnvStore
|
||||
if cmd.Local {
|
||||
store = loadLocalEnvStore()
|
||||
internal.Log.Printf("Adding environment variable '%s' to local vault", cmd.Name)
|
||||
} else {
|
||||
store = loadGlobalEnvStore()
|
||||
internal.Log.Printf("Adding environment variable '%s' to global vault", cmd.Name)
|
||||
}
|
||||
|
||||
if cmd.Value == "" {
|
||||
fmt.Fprintf(os.Stderr, "Enter value for %s: ", cmd.Name)
|
||||
inputValue, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Fprintln(os.Stderr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value = string(inputValue)
|
||||
cmd.Value = string(inputValue)
|
||||
}
|
||||
store.Vars[cmd.Name] = cmd.Value
|
||||
|
||||
if cmd.Local {
|
||||
saveLocalEnvStore(store)
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' added to local vault.\n", cmd.Name)
|
||||
} else {
|
||||
saveGlobalEnvStore(store)
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' added to global vault.\n", cmd.Name)
|
||||
}
|
||||
store.Vars[name] = value
|
||||
saveEnvStore(store)
|
||||
} // }}}
|
||||
|
||||
func subcommandRmEnvVar(name string) { // {{{
|
||||
store := loadEnvStore()
|
||||
if _, exists := store.Vars[name]; exists {
|
||||
delete(store.Vars, name)
|
||||
saveEnvStore(store)
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed.\n", name)
|
||||
func subcommandRmEnvVar(name string, localOnly bool) { // {{{
|
||||
localExists := false
|
||||
|
||||
// Check local store if it exists
|
||||
if _, err := os.Stat(localDbFilePath); err == nil {
|
||||
localStore := loadLocalEnvStore()
|
||||
if _, exists := localStore.Vars[name]; exists {
|
||||
delete(localStore.Vars, name)
|
||||
saveLocalEnvStore(localStore)
|
||||
localExists = true
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed from local vault.\n", name)
|
||||
} else if localOnly {
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in local vault.\n", name)
|
||||
}
|
||||
} else if localOnly {
|
||||
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
|
||||
}
|
||||
|
||||
// Check global store if not local-only
|
||||
if !localOnly {
|
||||
globalStore := loadGlobalEnvStore()
|
||||
if _, exists := globalStore.Vars[name]; exists {
|
||||
delete(globalStore.Vars, name)
|
||||
saveGlobalEnvStore(globalStore)
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed from global vault.\n", name)
|
||||
} else if !localExists {
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in global vault.\n", name)
|
||||
}
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func subcommandTagEnvVar(cmd TagCmd) { // {{{
|
||||
var store *EnvStore
|
||||
|
||||
// Determine which store to use
|
||||
if cmd.Local {
|
||||
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
store = loadLocalEnvStore()
|
||||
internal.Log.Printf("Using local vault")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found.\n", name)
|
||||
store = loadGlobalEnvStore()
|
||||
internal.Log.Printf("Using global vault")
|
||||
}
|
||||
|
||||
// Check if the variable exists
|
||||
if _, exists := store.Vars[cmd.Name]; !exists {
|
||||
vaultType := "global"
|
||||
if cmd.Local {
|
||||
vaultType = "local"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in %s vault.\n",
|
||||
cmd.Name,
|
||||
vaultType)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// If list flag is set, just list the tags and return
|
||||
if cmd.List {
|
||||
tags := store.GetTags(cmd.Name)
|
||||
if len(tags) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No tags for '%s'\n", cmd.Name)
|
||||
return
|
||||
}
|
||||
for _, tag := range tags {
|
||||
fmt.Println(tag)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If no tags provided and not in list mode, show error
|
||||
if len(cmd.Tags) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No tags provided. Use --list to view tags.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Process tags
|
||||
if cmd.Remove {
|
||||
internal.Log.Printf("Removing tags %v from variable '%s'", cmd.Tags, cmd.Name)
|
||||
store.RemoveTags(cmd.Name, cmd.Tags)
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"Removed tags from '%s': %s\n",
|
||||
cmd.Name,
|
||||
strings.Join(cmd.Tags, ", "),
|
||||
)
|
||||
} else {
|
||||
internal.Log.Printf("Adding tags %v to variable '%s'", cmd.Tags, cmd.Name)
|
||||
store.SetTags(cmd.Name, cmd.Tags)
|
||||
fmt.Fprintf(os.Stderr, "Added tags to '%s': %s\n", cmd.Name, strings.Join(cmd.Tags, ", "))
|
||||
}
|
||||
|
||||
// Save the store
|
||||
if cmd.Local {
|
||||
saveLocalEnvStore(store)
|
||||
} else {
|
||||
saveGlobalEnvStore(store)
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func subcommandListEnvVars(cmdArgs ListCmd) { // {{{
|
||||
store := loadEnvStore()
|
||||
for k, v := range store.Vars {
|
||||
var store *EnvStore
|
||||
|
||||
// Determine which store to use
|
||||
if cmdArgs.Local {
|
||||
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
|
||||
return
|
||||
}
|
||||
store = loadLocalEnvStore()
|
||||
} else {
|
||||
// Default: load and merge stores
|
||||
store = loadEnvStore()
|
||||
}
|
||||
|
||||
// Sort keys for consistent output
|
||||
var keys []string
|
||||
for k := range store.Vars {
|
||||
// Filter by tags if specified
|
||||
if store.HasAnyTag(k, cmdArgs.Tags) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := store.Vars[k]
|
||||
tags := store.GetTags(k)
|
||||
tagStr := ""
|
||||
if len(tags) > 0 {
|
||||
tagStr = fmt.Sprintf(" (tags: %s)", strings.Join(tags, ", "))
|
||||
}
|
||||
if cmdArgs.Values {
|
||||
fmt.Printf("%s=%s\n", k, v)
|
||||
fmt.Printf("%s=%s", k, v)
|
||||
fmt.Fprintf(os.Stderr, "%s", tagStr)
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf("%s\n", k)
|
||||
fmt.Printf("%s", k)
|
||||
fmt.Fprintf(os.Stderr, "%s", tagStr)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func subcommandExecCommand(cmdArgs ExecCmd) { // {{{
|
||||
store := loadEnvStore()
|
||||
var store *EnvStore
|
||||
|
||||
// Determine which store to use
|
||||
if cmdArgs.Local {
|
||||
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
store = loadLocalEnvStore()
|
||||
internal.Log.Println("Using local vault only for execution")
|
||||
} else {
|
||||
// Default: load and merge stores
|
||||
store = loadEnvStore()
|
||||
internal.Log.Println("Using merged vaults for execution")
|
||||
}
|
||||
envVars := os.Environ()
|
||||
for candidateEnvName, candidateEnvValue := range store.Vars {
|
||||
// Skip if not matching tag filter
|
||||
if !store.HasAnyTag(candidateEnvName, cmdArgs.Tags) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(cmdArgs.Env) == 0 {
|
||||
// If no env vars are specified, add all env vars
|
||||
envVars = append(envVars, fmt.Sprintf("%s=%s", candidateEnvName, candidateEnvValue))
|
||||
@ -266,6 +522,29 @@ func subcommandStartDaemon(timeoutMinutes int) { // {{{
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func subcommandLockVault() { // {{{
|
||||
internal.Log.Println("Locking vault by stopping daemon")
|
||||
|
||||
socketPath := daemon.GetSocketPath()
|
||||
client := daemon.NewClient(socketPath)
|
||||
|
||||
if !client.IsRunning() {
|
||||
internal.Log.Println("Daemon is not running, nothing to lock")
|
||||
fmt.Println("Vault is already locked (daemon not running)")
|
||||
return
|
||||
}
|
||||
|
||||
internal.Log.Println("Sending kill command to daemon")
|
||||
err := client.KillDaemon()
|
||||
if err != nil {
|
||||
internal.Log.Printf("Failed to kill daemon: %v", err)
|
||||
log.Fatalf("Failed to lock vault: %v", err)
|
||||
}
|
||||
|
||||
internal.Log.Println("Daemon stopped successfully")
|
||||
fmt.Println("Vault locked successfully")
|
||||
} // }}}
|
||||
|
||||
func subcommandRekeyVault() { // {{{
|
||||
// Ask for current master password
|
||||
fmt.Fprint(os.Stderr, "Enter current master password: ")
|
||||
@ -307,6 +586,21 @@ func subcommandRekeyVault() { // {{{
|
||||
log.Fatal("Passwords do not match")
|
||||
}
|
||||
|
||||
// Check if daemon is running and kill it
|
||||
socketPath := daemon.GetSocketPath()
|
||||
client := daemon.NewClient(socketPath)
|
||||
|
||||
if client.IsRunning() {
|
||||
internal.Log.Println("Daemon is running, stopping it before rekey")
|
||||
fmt.Println("Stopping password daemon before changing master password...")
|
||||
err := client.KillDaemon()
|
||||
if err != nil {
|
||||
internal.Log.Printf("Failed to kill daemon: %v", err)
|
||||
log.Fatalf("Failed to stop daemon: %v", err)
|
||||
}
|
||||
internal.Log.Println("Daemon stopped successfully")
|
||||
}
|
||||
|
||||
// Encrypt the master key with the new password
|
||||
newEncryptedKey := encryptKeyWithPassword(masterKey, newPassword)
|
||||
|
||||
@ -460,15 +754,16 @@ func decryptKeyWithPassword(encryptedKey, password []byte) ([]byte, error) { //
|
||||
return decrypted, nil
|
||||
} // }}}
|
||||
|
||||
func loadEnvStore() *EnvStore { // {{{
|
||||
func loadStoreFile(filePath string) *EnvStore { // {{{
|
||||
key := loadKey()
|
||||
if err := os.MkdirAll(filepath.Dir(dbFilePath), 0700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(dbFilePath)
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &EnvStore{Vars: make(map[string]string)}
|
||||
return &EnvStore{
|
||||
Vars: make(map[string]string),
|
||||
Tags: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -484,13 +779,61 @@ func loadEnvStore() *EnvStore { // {{{
|
||||
if err := json.Unmarshal(decrypted, &store); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Initialize Tags map if it's nil
|
||||
if store.Tags == nil {
|
||||
store.Tags = make(map[string][]string)
|
||||
}
|
||||
} else {
|
||||
return &EnvStore{Vars: make(map[string]string)}
|
||||
return &EnvStore{
|
||||
Vars: make(map[string]string),
|
||||
Tags: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
return &store
|
||||
} // }}}
|
||||
|
||||
func saveEnvStore(store *EnvStore) { // {{{
|
||||
func loadGlobalEnvStore() *EnvStore { // {{{
|
||||
if err := os.MkdirAll(filepath.Dir(dbFilePath), 0700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return loadStoreFile(dbFilePath)
|
||||
} // }}}
|
||||
|
||||
func loadLocalEnvStore() *EnvStore { // {{{
|
||||
return loadStoreFile(localDbFilePath)
|
||||
} // }}}
|
||||
|
||||
func loadEnvStore() *EnvStore { // {{{
|
||||
// Load global store first
|
||||
globalStore := loadGlobalEnvStore()
|
||||
|
||||
// Check if local store exists
|
||||
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
|
||||
// No local store, just return global
|
||||
return globalStore
|
||||
}
|
||||
|
||||
// Load local store
|
||||
localStore := loadLocalEnvStore()
|
||||
|
||||
// Merge stores (local takes precedence)
|
||||
mergedStore := &EnvStore{
|
||||
Vars: make(map[string]string),
|
||||
Tags: make(map[string][]string),
|
||||
}
|
||||
|
||||
// Copy all global vars and tags
|
||||
maps.Copy(mergedStore.Vars, globalStore.Vars)
|
||||
maps.Copy(mergedStore.Tags, globalStore.Tags)
|
||||
|
||||
// Override/add local vars and tags
|
||||
maps.Copy(mergedStore.Vars, localStore.Vars)
|
||||
maps.Copy(mergedStore.Tags, localStore.Tags)
|
||||
|
||||
return mergedStore
|
||||
} // }}}
|
||||
|
||||
func saveGlobalEnvStore(store *EnvStore) { // {{{
|
||||
key := loadKey()
|
||||
data, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
@ -507,3 +850,21 @@ func saveEnvStore(store *EnvStore) { // {{{
|
||||
log.Fatal(err)
|
||||
}
|
||||
} // }}}
|
||||
|
||||
func saveLocalEnvStore(store *EnvStore) { // {{{
|
||||
key := loadKey()
|
||||
data, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var nonce [24]byte
|
||||
if _, err := rand.Read(nonce[:]); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &key)
|
||||
|
||||
if err := os.WriteFile(localDbFilePath, encrypted, 0600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} // }}}
|
||||
|
@ -111,6 +111,29 @@ func (c *Client) RetrievePassword() ([]byte, error) {
|
||||
return []byte(resp.Data), nil
|
||||
}
|
||||
|
||||
// KillDaemon sends a kill command to the daemon to stop it
|
||||
func (c *Client) KillDaemon() error {
|
||||
internal.DaemonLog.Printf("Sending kill command to daemon")
|
||||
msg := Message{
|
||||
Command: "kill",
|
||||
}
|
||||
|
||||
internal.DaemonLog.Printf("Sending kill message to daemon")
|
||||
resp, err := c.sendMessage(msg)
|
||||
if err != nil {
|
||||
internal.DaemonLog.Printf("Failed to send kill message: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
internal.DaemonLog.Printf("Daemon reported error: %s", resp.Error)
|
||||
return fmt.Errorf("failed to kill daemon: %s", resp.Error)
|
||||
}
|
||||
|
||||
internal.DaemonLog.Printf("Kill command sent successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendMessage sends a message to the daemon and returns the response
|
||||
func (c *Client) sendMessage(msg Message) (*Response, error) {
|
||||
internal.DaemonLog.Printf("Connecting to daemon socket: %s", c.socketPath)
|
||||
|
@ -217,6 +217,23 @@ func handleConnection(conn net.Conn, cache *PasswordCache) {
|
||||
internal.DaemonLog.Println("Password retrieved, sending success response")
|
||||
encoder.Encode(Response{Success: true, Data: string(password)})
|
||||
|
||||
case "kill":
|
||||
internal.DaemonLog.Println("Kill command received, shutting down daemon")
|
||||
encoder.Encode(Response{Success: true})
|
||||
// Close the connection before exiting
|
||||
conn.Close()
|
||||
// Clean up the socket file
|
||||
os.Remove(conn.LocalAddr().String())
|
||||
internal.DaemonLog.Printf("Removed socket file: %s", conn.LocalAddr().String())
|
||||
// Exit with success status
|
||||
internal.DaemonLog.Println("Daemon shutting down gracefully")
|
||||
// Use goroutine to allow response to be sent before exit
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
return
|
||||
|
||||
default:
|
||||
internal.DaemonLog.Printf("Unknown command received: %s", msg.Command)
|
||||
encoder.Encode(Response{Success: false, Error: "Unknown command"})
|
||||
|
Loading…
x
Reference in New Issue
Block a user