From a6000c17f8c5c0c25190a8643981faf8d3ceb6da Mon Sep 17 00:00:00 2001 From: Muskan Pandey Date: Wed, 1 Apr 2026 19:25:12 +0530 Subject: [PATCH] [Bug]: Secret decryption fails with correct passphrase #40 --- forge-cli/cmd/secret.go | 61 +++++++++++-------- forge-core/secrets/encrypted_file_provider.go | 2 - 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/forge-cli/cmd/secret.go b/forge-cli/cmd/secret.go index 4c4be90..ceaace9 100644 --- a/forge-cli/cmd/secret.go +++ b/forge-cli/cmd/secret.go @@ -150,12 +150,29 @@ func defaultSecretsPath() string { return filepath.Join(home, ".forge", "secrets.enc") } -// secretsPathForDisplay returns the path being operated on for user-facing messages. -func secretsPathForDisplay() string { +// resolveSecretsPath returns the actual secrets file path that will be used, +// accounting for the --local flag and any secrets.path override in forge.yaml. +func resolveSecretsPath() string { if secretLocal { return localSecretsPath() } - return defaultSecretsPath() + path := defaultSecretsPath() + cfgPath := cfgFile + if !filepath.IsAbs(cfgPath) { + wd, _ := os.Getwd() + cfgPath = filepath.Join(wd, cfgPath) + } + if data, err := os.ReadFile(cfgPath); err == nil { + if cfg, err := parseSecretsPath(data); err == nil && cfg != "" { + path = cfg + } + } + return path +} + +// secretsPathForDisplay returns the path being operated on for user-facing messages. +func secretsPathForDisplay() string { + return resolveSecretsPath() } // resolvePassphrase returns the passphrase from FORGE_PASSPHRASE env or terminal prompt. @@ -176,35 +193,25 @@ func resolvePassphrase() (string, error) { // buildEncryptedProvider builds an EncryptedFileProvider using defaults or config. func buildEncryptedProvider() (*secrets.EncryptedFileProvider, error) { - var path string - if secretLocal { - path = localSecretsPath() - } else { - path = defaultSecretsPath() - - // Try loading config to get custom path - cfgPath := cfgFile - if !filepath.IsAbs(cfgPath) { - wd, _ := os.Getwd() - cfgPath = filepath.Join(wd, cfgPath) - } - if data, err := os.ReadFile(cfgPath); err == nil { - if cfg, err := parseSecretsPath(data); err == nil && cfg != "" { - path = cfg - } - } - } - - return secrets.NewEncryptedFileProvider(path, resolvePassphrase), nil + return secrets.NewEncryptedFileProvider(resolveSecretsPath(), resolvePassphrase), nil } // parseSecretsPath extracts secrets.path from raw YAML config bytes. +// It only looks for path: within the secrets: top-level block to avoid +// matching path: keys from other sections (e.g. skills.path). func parseSecretsPath(data []byte) (string, error) { - // Minimal parse to avoid importing config package + inSecrets := false for _, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "path:") && len(line) > 5 { - return strings.TrimSpace(line[5:]), nil + // A top-level key has no leading whitespace + if len(line) > 0 && line[0] != ' ' && line[0] != '\t' { + trimmed := strings.TrimSpace(line) + inSecrets = strings.HasPrefix(trimmed, "secrets:") + } + if inSecrets { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "path:") && len(trimmed) > 5 { + return strings.TrimSpace(trimmed[5:]), nil + } } } return "", nil diff --git a/forge-core/secrets/encrypted_file_provider.go b/forge-core/secrets/encrypted_file_provider.go index c6deb1a..5c29b01 100644 --- a/forge-core/secrets/encrypted_file_provider.go +++ b/forge-core/secrets/encrypted_file_provider.go @@ -114,7 +114,6 @@ func (p *EncryptedFileProvider) SetBatch(pairs map[string]string) error { func (p *EncryptedFileProvider) Delete(key string) error { p.mu.Lock() defer p.mu.Unlock() - if err := p.ensureLoaded(); err != nil { return err } @@ -132,7 +131,6 @@ func (p *EncryptedFileProvider) ensureLoaded() error { if p.loaded { return nil } - data, err := os.ReadFile(p.path) if os.IsNotExist(err) { // No file yet — start with an empty cache.