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"
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user