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:
@@ -26,7 +26,7 @@ const (
|
||||
SecretsDirName = ".secrets"
|
||||
)
|
||||
|
||||
// EncryptedData represents the structure of encrypted password file
|
||||
// EncryptedData represents the structure of an encrypted credential file
|
||||
type EncryptedData struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Nonce string `json:"nonce"`
|
||||
@@ -49,172 +49,229 @@ func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "pass_mgr",
|
||||
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")
|
||||
agentID = os.Getenv("AGENT_ID")
|
||||
|
||||
// Commands
|
||||
rootCmd.AddCommand(getCmd())
|
||||
rootCmd.AddCommand(generateCmd())
|
||||
rootCmd.AddCommand(unsetCmd())
|
||||
rootCmd.AddCommand(rotateCmd())
|
||||
rootCmd.AddCommand(adminInitCmd())
|
||||
rootCmd.AddCommand(listCmd())
|
||||
rootCmd.AddCommand(getSecretCmd())
|
||||
rootCmd.AddCommand(getUsernameCmd())
|
||||
rootCmd.AddCommand(setCmd())
|
||||
rootCmd.AddCommand(unsetCmd())
|
||||
rootCmd.AddCommand(generateCmd())
|
||||
rootCmd.AddCommand(rotateCmd())
|
||||
rootCmd.AddCommand(adminCmd())
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getCmd() *cobra.Command {
|
||||
var showUsername bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "get [key]",
|
||||
Short: "Get password for a key",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
key := args[0]
|
||||
password, user, err := getPassword(key)
|
||||
// ── Commands ────────────────────────────────────────────────────────────
|
||||
|
||||
func listCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all keys for this agent",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !isInitialized() {
|
||||
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
|
||||
}
|
||||
secretsDir := getSecretsDir()
|
||||
entries, err := os.ReadDir(secretsDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
if os.IsNotExist(err) {
|
||||
// No secrets yet — just output nothing
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read secrets directory: %w", err)
|
||||
}
|
||||
if showUsername {
|
||||
fmt.Println(user)
|
||||
} else {
|
||||
fmt.Println(password)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func generateCmd() *cobra.Command {
|
||||
var user string
|
||||
func getUsernameCmd() *cobra.Command {
|
||||
var key string
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate [key]",
|
||||
Short: "Generate a new password",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
key := args[0]
|
||||
// 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)
|
||||
Use: "get-username",
|
||||
Short: "Get username for a key",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
password, err := generatePassword(32)
|
||||
_, username, err := getCredential(key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
if err := setPassword(key, user, password); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(password)
|
||||
fmt.Print(username)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func unsetCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
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
|
||||
var key string
|
||||
cmd := &cobra.Command{
|
||||
Use: "set [key] [password]",
|
||||
Short: "Set password (admin only)",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// 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. 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)
|
||||
Use: "unset",
|
||||
Short: "Remove a credential",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -238,125 +295,41 @@ func isInitialized() bool {
|
||||
}
|
||||
|
||||
func loadAdminKey() ([]byte, error) {
|
||||
keyPath := getAdminKeyPath()
|
||||
key, err := os.ReadFile(keyPath)
|
||||
key, err := os.ReadFile(getAdminKeyPath())
|
||||
if err != nil {
|
||||
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)
|
||||
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 {
|
||||
if workspaceDir != "" && agentID != "" {
|
||||
return filepath.Join(workspaceDir, SecretsDirName, agentID)
|
||||
}
|
||||
// Fallback to home directory
|
||||
return filepath.Join(getHomeDir(), SecretsDirName, "default")
|
||||
}
|
||||
|
||||
func getPasswordFilePath(key string) string {
|
||||
func getCredentialFilePath(key string) string {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
|
||||
return &EncryptedData{
|
||||
Algorithm: DefaultAlgorithm,
|
||||
Nonce: base64.StdEncoding.EncodeToString(nonce),
|
||||
@@ -369,165 +342,174 @@ func decrypt(data *EncryptedData, key []byte) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := base64.StdEncoding.DecodeString(data.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
func setPassword(key, user, password string) error {
|
||||
// ── CRUD ────────────────────────────────────────────────────────────────
|
||||
|
||||
func setCredential(key, username, secret string) error {
|
||||
if !isInitialized() {
|
||||
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
|
||||
}
|
||||
|
||||
adminKey, err := loadAdminKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create secrets directory
|
||||
secretsDir := getSecretsDir()
|
||||
if err := os.MkdirAll(secretsDir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create secrets directory: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt password
|
||||
data := map[string]string{
|
||||
"password": password,
|
||||
"user": user,
|
||||
}
|
||||
plaintext, _ := json.Marshal(data)
|
||||
|
||||
encrypted, err := encrypt(plaintext, adminKey)
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"secret": secret,
|
||||
"username": username,
|
||||
})
|
||||
encrypted, err := encrypt(payload, adminKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt: %w", err)
|
||||
}
|
||||
encrypted.User = user
|
||||
|
||||
// Save to file
|
||||
filePath := getPasswordFilePath(key)
|
||||
encrypted.User = username
|
||||
fileData, _ := json.MarshalIndent(encrypted, "", " ")
|
||||
filePath := getCredentialFilePath(key)
|
||||
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
|
||||
}
|
||||
|
||||
func getPassword(key string) (string, string, error) {
|
||||
func getCredential(key string) (string, string, error) {
|
||||
if !isInitialized() {
|
||||
return "", "", fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
|
||||
}
|
||||
|
||||
adminKey, err := loadAdminKey()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
filePath := getPasswordFilePath(key)
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
fileData, err := os.ReadFile(getCredentialFilePath(key))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("password not found: %w", err)
|
||||
return "", "", fmt.Errorf("credential not found for key '%s'", key)
|
||||
}
|
||||
|
||||
var encrypted EncryptedData
|
||||
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)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
|
||||
var data map[string]string
|
||||
if err := json.Unmarshal(plaintext, &data); err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse decrypted data: %w", err)
|
||||
}
|
||||
|
||||
return data["password"], data["user"], nil
|
||||
return data["secret"], data["username"], nil
|
||||
}
|
||||
|
||||
func removePassword(key string) error {
|
||||
func removeCredential(key string) error {
|
||||
if !isInitialized() {
|
||||
return fmt.Errorf("pass_mgr not initialized. Run 'pass_mgr admin init' first")
|
||||
}
|
||||
|
||||
filePath := getPasswordFilePath(key)
|
||||
filePath := getCredentialFilePath(key)
|
||||
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
|
||||
}
|
||||
|
||||
func generatePassword(length int) (string, error) {
|
||||
func generateRandomPassword(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
|
||||
password := make([]byte, length)
|
||||
for i := range password {
|
||||
randomByte := make([]byte, 1)
|
||||
if _, err := rand.Read(randomByte); err != nil {
|
||||
b := make([]byte, 1)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
password[i] = charset[int(randomByte[0])%len(charset)]
|
||||
password[i] = charset[int(b[0])%len(charset)]
|
||||
}
|
||||
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 {
|
||||
// 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()
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Implement actual leak detection
|
||||
// For now, just check if content contains common patterns
|
||||
return strings.Contains(content, "admin") && strings.Contains(content, "password")
|
||||
// TODO: Implement actual leak detection against stored key
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetOnLeak resets pass_mgr to uninitialized state and logs security breach
|
||||
// ResetOnLeak resets pass_mgr to uninitialized state
|
||||
func ResetOnLeak() error {
|
||||
configPath := getConfigPath()
|
||||
|
||||
// Remove config (but keep key file for potential recovery)
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
if err := os.Remove(getConfigPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Log security breach
|
||||
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))
|
||||
|
||||
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(logEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = f.WriteString(entry)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -58,8 +58,15 @@ export interface PcExecError extends Error {
|
||||
function extractPassMgrGets(command: string): 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 = [
|
||||
// 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,
|
||||
/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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
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'],
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
Reference in New Issue
Block a user