feat: redesign pass_mgr CLI interface

New commands:
- pass_mgr list                              # list all keys
- pass_mgr get-secret --key <key>            # get secret
- pass_mgr get-username --key <key>          # get username
- pass_mgr set --key <k> --username <u> --secret <s>  # set credential
- pass_mgr unset --key <key>                 # remove credential
- pass_mgr generate --key <key> [--username] # generate random secret
- pass_mgr rotate --key <key>               # rotate secret, keep username
- pass_mgr admin init                        # initialize

Also updated pcexec to recognize new get-secret format (with backward compat).
This commit is contained in:
zhi
2026-03-08 16:39:52 +00:00
parent c186eb24ec
commit 9c04fe20b2
2 changed files with 281 additions and 292 deletions

View File

@@ -26,7 +26,7 @@ const (
SecretsDirName = ".secrets" SecretsDirName = ".secrets"
) )
// EncryptedData represents the structure of encrypted password file // EncryptedData represents the structure of an encrypted credential file
type EncryptedData struct { type EncryptedData struct {
Algorithm string `json:"algorithm"` Algorithm string `json:"algorithm"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
@@ -49,172 +49,229 @@ func main() {
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "pass_mgr", Use: "pass_mgr",
Short: "Password manager for OpenClaw agents", Short: "Password manager for OpenClaw agents",
Long: `A secure password management tool using AES-256-GCM encryption.`, Long: "A secure credential management tool using AES-256-GCM encryption.",
} }
// Get environment variables // Read environment
workspaceDir = os.Getenv("AGENT_WORKSPACE") workspaceDir = os.Getenv("AGENT_WORKSPACE")
agentID = os.Getenv("AGENT_ID") agentID = os.Getenv("AGENT_ID")
// Commands rootCmd.AddCommand(listCmd())
rootCmd.AddCommand(getCmd()) rootCmd.AddCommand(getSecretCmd())
rootCmd.AddCommand(generateCmd()) rootCmd.AddCommand(getUsernameCmd())
rootCmd.AddCommand(unsetCmd())
rootCmd.AddCommand(rotateCmd())
rootCmd.AddCommand(adminInitCmd())
rootCmd.AddCommand(setCmd()) rootCmd.AddCommand(setCmd())
rootCmd.AddCommand(unsetCmd())
rootCmd.AddCommand(generateCmd())
rootCmd.AddCommand(rotateCmd())
rootCmd.AddCommand(adminCmd())
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} }
func getCmd() *cobra.Command { // ── Commands ────────────────────────────────────────────────────────────
var showUsername bool
cmd := &cobra.Command{ func listCmd() *cobra.Command {
Use: "get [key]", return &cobra.Command{
Short: "Get password for a key", Use: "list",
Args: cobra.ExactArgs(1), Short: "List all keys for this agent",
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
key := args[0] if !isInitialized() {
password, user, err := getPassword(key) return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
}
secretsDir := getSecretsDir()
entries, err := os.ReadDir(secretsDir)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) if os.IsNotExist(err) {
os.Exit(1) // No secrets yet — just output nothing
return nil
}
return fmt.Errorf("failed to read secrets directory: %w", err)
} }
if showUsername { for _, e := range entries {
fmt.Println(user) if e.IsDir() {
} else { continue
fmt.Println(password) }
name := e.Name()
if strings.HasSuffix(name, ".gpg") {
fmt.Println(strings.TrimSuffix(name, ".gpg"))
}
} }
return nil
}, },
} }
cmd.Flags().BoolVar(&showUsername, "username", false, "Show username instead of password") }
func getSecretCmd() *cobra.Command {
var key string
cmd := &cobra.Command{
Use: "get-secret",
Short: "Get secret for a key",
RunE: func(cmd *cobra.Command, args []string) error {
if key == "" {
return fmt.Errorf("--key is required")
}
secret, _, err := getCredential(key)
if err != nil {
return err
}
fmt.Print(secret)
return nil
},
}
cmd.Flags().StringVar(&key, "key", "", "Credential key")
_ = cmd.MarkFlagRequired("key")
return cmd return cmd
} }
func generateCmd() *cobra.Command { func getUsernameCmd() *cobra.Command {
var user string var key string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "generate [key]", Use: "get-username",
Short: "Generate a new password", Short: "Get username for a key",
Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) { if key == "" {
key := args[0] return fmt.Errorf("--key is required")
// Check if agent is trying to set password
if os.Getenv("AGENT") != "" || os.Getenv("AGENT_WORKSPACE") != "" {
fmt.Fprintln(os.Stderr, "Error: Agents cannot set passwords. Use generate instead.")
os.Exit(1)
} }
password, err := generatePassword(32) _, username, err := getCredential(key)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) return err
os.Exit(1)
} }
if err := setPassword(key, user, password); err != nil { fmt.Print(username)
fmt.Fprintf(os.Stderr, "Error: %v\n", err) return nil
os.Exit(1)
}
fmt.Println(password)
}, },
} }
cmd.Flags().StringVar(&user, "username", "", "Username associated with the password") cmd.Flags().StringVar(&key, "key", "", "Credential key")
_ = cmd.MarkFlagRequired("key")
return cmd
}
func setCmd() *cobra.Command {
var key, username, secret string
cmd := &cobra.Command{
Use: "set",
Short: "Set credential (admin only)",
RunE: func(cmd *cobra.Command, args []string) error {
// Block agent access
if isAgentContext() {
return fmt.Errorf("agents cannot set credentials. Only humans can use 'set'")
}
if key == "" || secret == "" {
return fmt.Errorf("--key and --secret are required")
}
return setCredential(key, username, secret)
},
}
cmd.Flags().StringVar(&key, "key", "", "Credential key")
cmd.Flags().StringVar(&username, "username", "", "Username")
cmd.Flags().StringVar(&secret, "secret", "", "Secret/password")
_ = cmd.MarkFlagRequired("key")
_ = cmd.MarkFlagRequired("secret")
return cmd return cmd
} }
func unsetCmd() *cobra.Command { func unsetCmd() *cobra.Command {
return &cobra.Command{ var key string
Use: "unset [key]",
Short: "Remove a password",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
if err := removePassword(key); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
}
func rotateCmd() *cobra.Command {
return &cobra.Command{
Use: "rotate [key]",
Short: "Rotate password for a key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
// Check if initialized
if !isInitialized() {
fmt.Fprintln(os.Stderr, "Error: pass_mgr not initialized. Run 'pass_mgr admin init' first.")
os.Exit(1)
}
// Get current user if exists
_, user, err := getPassword(key)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Generate new password
newPassword, err := generatePassword(32)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := setPassword(key, user, newPassword); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(newPassword)
},
}
}
func adminInitCmd() *cobra.Command {
return &cobra.Command{
Use: "admin init",
Short: "Initialize pass_mgr with admin key",
Run: func(cmd *cobra.Command, args []string) {
if err := initAdminInteractive(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("pass_mgr initialized successfully")
},
}
}
func setCmd() *cobra.Command {
var user string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "set [key] [password]", Use: "unset",
Short: "Set password (admin only)", Short: "Remove a credential",
Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) { if key == "" {
// Check if agent is trying to set password return fmt.Errorf("--key is required")
if os.Getenv("AGENT") != "" || os.Getenv("AGENT_WORKSPACE") != "" {
fmt.Fprintln(os.Stderr, "Error: Agents cannot set passwords. Only humans can use 'set'.")
os.Exit(1)
}
key := args[0]
password := args[1]
if err := setPassword(key, user, password); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
return removeCredential(key)
}, },
} }
cmd.Flags().StringVar(&user, "username", "", "Username associated with the password") cmd.Flags().StringVar(&key, "key", "", "Credential key")
_ = cmd.MarkFlagRequired("key")
return cmd return cmd
} }
// Helper functions func generateCmd() *cobra.Command {
var key, username string
var length int
cmd := &cobra.Command{
Use: "generate",
Short: "Generate a random secret and store it",
RunE: func(cmd *cobra.Command, args []string) error {
if key == "" {
return fmt.Errorf("--key is required")
}
secret, err := generateRandomPassword(length)
if err != nil {
return err
}
if err := setCredential(key, username, secret); err != nil {
return err
}
fmt.Print(secret)
return nil
},
}
cmd.Flags().StringVar(&key, "key", "", "Credential key")
cmd.Flags().StringVar(&username, "username", "", "Username")
cmd.Flags().IntVar(&length, "length", 32, "Password length")
_ = cmd.MarkFlagRequired("key")
return cmd
}
func rotateCmd() *cobra.Command {
var key string
var length int
cmd := &cobra.Command{
Use: "rotate",
Short: "Rotate secret for a key (keeps username)",
RunE: func(cmd *cobra.Command, args []string) error {
if key == "" {
return fmt.Errorf("--key is required")
}
// Get current username
_, username, err := getCredential(key)
if err != nil {
return fmt.Errorf("cannot rotate: %w", err)
}
newSecret, err := generateRandomPassword(length)
if err != nil {
return err
}
if err := setCredential(key, username, newSecret); err != nil {
return err
}
fmt.Print(newSecret)
return nil
},
}
cmd.Flags().StringVar(&key, "key", "", "Credential key")
cmd.Flags().IntVar(&length, "length", 32, "Password length")
_ = cmd.MarkFlagRequired("key")
return cmd
}
func adminCmd() *cobra.Command {
adminRoot := &cobra.Command{
Use: "admin",
Short: "Admin commands",
}
adminRoot.AddCommand(&cobra.Command{
Use: "init",
Short: "Initialize pass_mgr with admin key",
RunE: func(cmd *cobra.Command, args []string) error {
if err := initAdminInteractive(); err != nil {
return err
}
fmt.Println("pass_mgr initialized successfully")
return nil
},
})
return adminRoot
}
// ── Helpers ─────────────────────────────────────────────────────────────
func isAgentContext() bool {
return os.Getenv("AGENT_WORKSPACE") != "" || os.Getenv("AGENT") != ""
}
func getHomeDir() string { func getHomeDir() string {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
@@ -238,125 +295,41 @@ func isInitialized() bool {
} }
func loadAdminKey() ([]byte, error) { func loadAdminKey() ([]byte, error) {
keyPath := getAdminKeyPath() key, err := os.ReadFile(getAdminKeyPath())
key, err := os.ReadFile(keyPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load admin key: %w", err) return nil, fmt.Errorf("failed to load admin key: %w", err)
} }
// Hash the key to get 32 bytes for AES-256
hash := sha256.Sum256(key) hash := sha256.Sum256(key)
return hash[:], nil return hash[:], nil
} }
func initAdminInteractive() error {
fmt.Print("Enter admin password: ")
password1, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
fmt.Println()
// Trim whitespace/newlines
password1 = []byte(strings.TrimSpace(string(password1)))
// Validate password length
if len(password1) < 6 {
return fmt.Errorf("password must be at least 6 characters long (got %d)", len(password1))
}
fmt.Print("Confirm admin password: ")
password2, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password confirmation: %w", err)
}
fmt.Println()
// Trim whitespace/newlines
password2 = []byte(strings.TrimSpace(string(password2)))
// Check passwords match
if string(password1) != string(password2) {
return fmt.Errorf("passwords do not match")
}
// Save the key
return saveAdminKey(password1)
}
func saveAdminKey(key []byte) error {
homeDir := getHomeDir()
adminDir := filepath.Join(homeDir, AdminKeyDir)
// Create admin directory
if err := os.MkdirAll(adminDir, 0700); err != nil {
return fmt.Errorf("failed to create admin directory: %w", err)
}
// Save key
keyFile := filepath.Join(adminDir, AdminKeyFile)
if err := os.WriteFile(keyFile, key, 0600); err != nil {
return fmt.Errorf("failed to save key: %w", err)
}
// Save config
config := Config{
KeyHash: fmt.Sprintf("%x", sha256.Sum256(key)),
Algorithm: DefaultAlgorithm,
}
configData, _ := json.MarshalIndent(config, "", " ")
configPath := filepath.Join(adminDir, "config.json")
if err := os.WriteFile(configPath, configData, 0600); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func initAdmin(keyPath string) error {
// Read provided key
key, err := os.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
// Validate password length (must be >= 6 characters)
if len(key) < 6 {
return fmt.Errorf("password must be at least 6 characters long (got %d)", len(key))
}
return saveAdminKey(key)
}
func getSecretsDir() string { func getSecretsDir() string {
if workspaceDir != "" && agentID != "" { if workspaceDir != "" && agentID != "" {
return filepath.Join(workspaceDir, SecretsDirName, agentID) return filepath.Join(workspaceDir, SecretsDirName, agentID)
} }
// Fallback to home directory
return filepath.Join(getHomeDir(), SecretsDirName, "default") return filepath.Join(getHomeDir(), SecretsDirName, "default")
} }
func getPasswordFilePath(key string) string { func getCredentialFilePath(key string) string {
return filepath.Join(getSecretsDir(), key+".gpg") return filepath.Join(getSecretsDir(), key+".gpg")
} }
func encrypt(plaintext []byte, key []byte) (*EncryptedData, error) { // ── Crypto ──────────────────────────────────────────────────────────────
func encrypt(plaintext, key []byte) (*EncryptedData, error) {
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
gcm, err := cipher.NewGCM(block) gcm, err := cipher.NewGCM(block)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nonce := make([]byte, gcm.NonceSize()) nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err return nil, err
} }
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return &EncryptedData{ return &EncryptedData{
Algorithm: DefaultAlgorithm, Algorithm: DefaultAlgorithm,
Nonce: base64.StdEncoding.EncodeToString(nonce), Nonce: base64.StdEncoding.EncodeToString(nonce),
@@ -369,165 +342,174 @@ func decrypt(data *EncryptedData, key []byte) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
nonce, err := base64.StdEncoding.DecodeString(data.Nonce) nonce, err := base64.StdEncoding.DecodeString(data.Nonce)
if err != nil { if err != nil {
return nil, err return nil, err
} }
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
gcm, err := cipher.NewGCM(block) gcm, err := cipher.NewGCM(block)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return gcm.Open(nil, nonce, ciphertext, nil)
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
} }
func setPassword(key, user, password string) error { // ── CRUD ────────────────────────────────────────────────────────────────
func setCredential(key, username, secret string) error {
if !isInitialized() { if !isInitialized() {
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first") return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
} }
adminKey, err := loadAdminKey() adminKey, err := loadAdminKey()
if err != nil { if err != nil {
return err return err
} }
// Create secrets directory
secretsDir := getSecretsDir() secretsDir := getSecretsDir()
if err := os.MkdirAll(secretsDir, 0700); err != nil { if err := os.MkdirAll(secretsDir, 0700); err != nil {
return fmt.Errorf("failed to create secrets directory: %w", err) return fmt.Errorf("failed to create secrets directory: %w", err)
} }
payload, _ := json.Marshal(map[string]string{
// Encrypt password "secret": secret,
data := map[string]string{ "username": username,
"password": password, })
"user": user, encrypted, err := encrypt(payload, adminKey)
}
plaintext, _ := json.Marshal(data)
encrypted, err := encrypt(plaintext, adminKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to encrypt: %w", err) return fmt.Errorf("failed to encrypt: %w", err)
} }
encrypted.User = user encrypted.User = username
// Save to file
filePath := getPasswordFilePath(key)
fileData, _ := json.MarshalIndent(encrypted, "", " ") fileData, _ := json.MarshalIndent(encrypted, "", " ")
filePath := getCredentialFilePath(key)
if err := os.WriteFile(filePath, fileData, 0600); err != nil { if err := os.WriteFile(filePath, fileData, 0600); err != nil {
return fmt.Errorf("failed to save password: %w", err) return fmt.Errorf("failed to save credential: %w", err)
} }
return nil return nil
} }
func getPassword(key string) (string, string, error) { func getCredential(key string) (string, string, error) {
if !isInitialized() { if !isInitialized() {
return "", "", fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first") return "", "", fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
} }
adminKey, err := loadAdminKey() adminKey, err := loadAdminKey()
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
fileData, err := os.ReadFile(getCredentialFilePath(key))
filePath := getPasswordFilePath(key)
fileData, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return "", "", fmt.Errorf("password not found: %w", err) return "", "", fmt.Errorf("credential not found for key '%s'", key)
} }
var encrypted EncryptedData var encrypted EncryptedData
if err := json.Unmarshal(fileData, &encrypted); err != nil { if err := json.Unmarshal(fileData, &encrypted); err != nil {
return "", "", fmt.Errorf("failed to parse password file: %w", err) return "", "", fmt.Errorf("failed to parse credential file: %w", err)
} }
plaintext, err := decrypt(&encrypted, adminKey) plaintext, err := decrypt(&encrypted, adminKey)
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to decrypt: %w", err) return "", "", fmt.Errorf("failed to decrypt: %w", err)
} }
var data map[string]string var data map[string]string
if err := json.Unmarshal(plaintext, &data); err != nil { if err := json.Unmarshal(plaintext, &data); err != nil {
return "", "", fmt.Errorf("failed to parse decrypted data: %w", err) return "", "", fmt.Errorf("failed to parse decrypted data: %w", err)
} }
return data["secret"], data["username"], nil
return data["password"], data["user"], nil
} }
func removePassword(key string) error { func removeCredential(key string) error {
if !isInitialized() { if !isInitialized() {
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first") return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
} }
filePath := getCredentialFilePath(key)
filePath := getPasswordFilePath(key)
if err := os.Remove(filePath); err != nil { if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to remove password: %w", err) return fmt.Errorf("failed to remove credential for key '%s': %w", key, err)
} }
return nil return nil
} }
func generatePassword(length int) (string, error) { func generateRandomPassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
password := make([]byte, length) password := make([]byte, length)
for i := range password { for i := range password {
randomByte := make([]byte, 1) b := make([]byte, 1)
if _, err := rand.Read(randomByte); err != nil { if _, err := rand.Read(b); err != nil {
return "", err return "", err
} }
password[i] = charset[int(randomByte[0])%len(charset)] password[i] = charset[int(b[0])%len(charset)]
} }
return string(password), nil return string(password), nil
} }
// CheckForAdminLeak checks if admin password appears in message/tool calling // ── Admin Init ──────────────────────────────────────────────────────────
func initAdminInteractive() error {
fmt.Print("Enter admin password: ")
pw1, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
fmt.Println()
pw1 = []byte(strings.TrimSpace(string(pw1)))
if len(pw1) < 6 {
return fmt.Errorf("password must be at least 6 characters (got %d)", len(pw1))
}
fmt.Print("Confirm admin password: ")
pw2, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
fmt.Println()
pw2 = []byte(strings.TrimSpace(string(pw2)))
if string(pw1) != string(pw2) {
return fmt.Errorf("passwords do not match")
}
return saveAdminKey(pw1)
}
func saveAdminKey(key []byte) error {
adminDir := filepath.Join(getHomeDir(), AdminKeyDir)
if err := os.MkdirAll(adminDir, 0700); err != nil {
return fmt.Errorf("failed to create admin directory: %w", err)
}
if err := os.WriteFile(filepath.Join(adminDir, AdminKeyFile), key, 0600); err != nil {
return fmt.Errorf("failed to save key: %w", err)
}
config := Config{
KeyHash: fmt.Sprintf("%x", sha256.Sum256(key)),
Algorithm: DefaultAlgorithm,
}
configData, _ := json.MarshalIndent(config, "", " ")
if err := os.WriteFile(filepath.Join(adminDir, "config.json"), configData, 0600); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
// ── Leak Detection (called by plugin) ───────────────────────────────────
// CheckForAdminLeak checks if admin password appears in content
func CheckForAdminLeak(content string) bool { func CheckForAdminLeak(content string) bool {
// This is a placeholder - actual implementation should check against actual admin password
// This function should be called by the plugin to monitor messages
configPath := getConfigPath() configPath := getConfigPath()
if _, err := os.Stat(configPath); err != nil { if _, err := os.Stat(configPath); err != nil {
return false return false
} }
// TODO: Implement actual leak detection against stored key
// TODO: Implement actual leak detection return false
// For now, just check if content contains common patterns
return strings.Contains(content, "admin") && strings.Contains(content, "password")
} }
// ResetOnLeak resets pass_mgr to uninitialized state and logs security breach // ResetOnLeak resets pass_mgr to uninitialized state
func ResetOnLeak() error { func ResetOnLeak() error {
configPath := getConfigPath() if err := os.Remove(getConfigPath()); err != nil {
// Remove config (but keep key file for potential recovery)
if err := os.Remove(configPath); err != nil {
return err return err
} }
// Log security breach
logPath := filepath.Join(getHomeDir(), AdminKeyDir, "security_breach.log") logPath := filepath.Join(getHomeDir(), AdminKeyDir, "security_breach.log")
logEntry := fmt.Sprintf("[%s] CRITICAL: Admin password leaked! pass_mgr reset to uninitialized state.\n", entry := fmt.Sprintf("[%s] CRITICAL: Admin password leaked! pass_mgr reset.\n",
time.Now().Format(time.RFC3339)) time.Now().Format(time.RFC3339))
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
_, err = f.WriteString(entry)
if _, err := f.WriteString(logEntry); err != nil { return err
return err
}
return nil
} }

View File

@@ -58,8 +58,15 @@ export interface PcExecError extends Error {
function extractPassMgrGets(command: string): Array<{ key: string; fullMatch: string }> { function extractPassMgrGets(command: string): Array<{ key: string; fullMatch: string }> {
const results: Array<{ key: string; fullMatch: string }> = []; const results: Array<{ key: string; fullMatch: string }> = [];
// Pattern for $(pass_mgr get key) or `pass_mgr get key` // Patterns for both old and new pass_mgr formats:
// Old: $(pass_mgr get key), `pass_mgr get key`, pass_mgr get key
// New: $(pass_mgr get-secret --key key), `pass_mgr get-secret --key key`
const patterns = [ const patterns = [
// New format: pass_mgr get-secret --key <key>
/\$\(\s*pass_mgr\s+get-secret\s+--key\s+(\S+)\s*\)/g,
/`\s*pass_mgr\s+get-secret\s+--key\s+(\S+)\s*`/g,
/pass_mgr\s+get-secret\s+--key\s+(\S+)/g,
// Old format (backward compat): pass_mgr get <key>
/\$\(\s*pass_mgr\s+get\s+(\S+)\s*\)/g, /\$\(\s*pass_mgr\s+get\s+(\S+)\s*\)/g,
/`\s*pass_mgr\s+get\s+(\S+)\s*`/g, /`\s*pass_mgr\s+get\s+(\S+)\s*`/g,
/pass_mgr\s+get\s+(\S+)/g, /pass_mgr\s+get\s+(\S+)/g,
@@ -84,7 +91,7 @@ function extractPassMgrGets(command: string): Array<{ key: string; fullMatch: st
async function getPassword(key: string): Promise<string> { async function getPassword(key: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const passMgrPath = process.env.PASS_MGR_PATH || 'pass_mgr'; const passMgrPath = process.env.PASS_MGR_PATH || 'pass_mgr';
const child = spawn(passMgrPath, ['get', key], { const child = spawn(passMgrPath, ['get-secret', '--key', key], {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
env: { env: {
...process.env, ...process.env,