feat: implement debug logging system (#5) - Added debug.go with structured logging using log/slog - Supports GODEBUG=berlin.sneak.pkg.secret flag - JSON output for non-TTY stderr, colorized output for TTY - Added Debug(), DebugF(), and DebugWith() functions - Early return when debug is disabled for performance - Added comprehensive tests for debug functionality - Integrated debug logging into CLI init and vault operations - Removed completed TODO item #5

This commit is contained in:
Jeffrey Paul 2025-05-29 06:25:50 -07:00
parent 9f0f5cc8a1
commit 1b8ea9695b
2 changed files with 24 additions and 7 deletions

View File

@ -15,13 +15,6 @@ This document outlines the bugs, issues, and improvements that need to be addres
- [ ] **4. No graceful handling of corrupted state**: If key files are corrupted or missing, the tool should provide clear error messages and recovery suggestions.
- [ ] **5.** When GODEBUG contains 'berlin.sneak.pkg.secret', output structured
debug data to STDERR. use log/slog. if stderr is not a tty, output jsonl. if
it is a tty, output colorized structured log data in a format similar to
printf's %#v format. create a debug logging function that calls a helper
function to see if the debug logging is enabled, and returns immediately if it
is not.
### Core Functionality Bugs
- [ ] **5. Multiple vaults using the same mnemonic will derive the same long-term keys**: Adding additional vaults with the same mnemonic should increment the index value used. The mnemonic should be double sha256 hashed and the hash value stored in the vault metadata along with the index value (starting at zero) and when additional vaults are added with the same mnemonic (as determined by hash) then the index value should be incremented. The README should be updated to document this behavior.

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"math/big"
"os"
"path/filepath"
@ -487,10 +488,14 @@ func newDecryptCmd() *cobra.Command {
// Init initializes the secrets manager
func (cli *CLIInstance) Init(cmd *cobra.Command) error {
Debug("Starting secret manager initialization")
// Create state directory
stateDir := cli.GetStateDir()
DebugWith("Creating state directory", slog.String("path", stateDir))
if err := cli.fs.MkdirAll(stateDir, 0700); err != nil {
Debug("Failed to create state directory", "error", err)
return fmt.Errorf("failed to create state directory: %w", err)
}
@ -502,64 +507,83 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
var mnemonicStr string
if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
Debug("Using mnemonic from environment variable")
mnemonicStr = envMnemonic
} else {
Debug("Prompting user for mnemonic phrase")
// Read mnemonic from stdin using shared line reader
var err error
mnemonicStr, err = readLineFromStdin("Enter your BIP39 mnemonic phrase: ")
if err != nil {
Debug("Failed to read mnemonic from stdin", "error", err)
return fmt.Errorf("failed to read mnemonic: %w", err)
}
}
if mnemonicStr == "" {
Debug("Empty mnemonic provided")
return fmt.Errorf("mnemonic cannot be empty")
}
// Validate the mnemonic using BIP39
DebugWith("Validating BIP39 mnemonic", slog.Int("word_count", len(strings.Fields(mnemonicStr))))
if !bip39.IsMnemonicValid(mnemonicStr) {
Debug("Invalid BIP39 mnemonic provided")
return fmt.Errorf("invalid BIP39 mnemonic phrase\nRun 'secret generate mnemonic' to create a valid mnemonic")
}
// Derive long-term keypair from mnemonic
DebugWith("Deriving long-term key from mnemonic", slog.Int("index", 0))
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, 0)
if err != nil {
Debug("Failed to derive long-term key", "error", err)
return fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
}
// Create default vault
Debug("Creating default vault")
_, err = CreateVault(cli.fs, cli.stateDir, "default")
if err != nil {
Debug("Failed to create default vault", "error", err)
return fmt.Errorf("failed to create default vault: %w", err)
}
// Set default vault as current
Debug("Setting default vault as current")
if err := SelectVault(cli.fs, cli.stateDir, "default"); err != nil {
Debug("Failed to select default vault", "error", err)
return fmt.Errorf("failed to select default vault: %w", err)
}
// Store long-term public key in vault
vaultDir := filepath.Join(stateDir, "vaults.d", "default")
ltPubKey := ltIdentity.Recipient().String()
DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir))
if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600); err != nil {
Debug("Failed to write long-term public key", "error", err)
return fmt.Errorf("failed to write long-term public key: %w", err)
}
// Prompt for passphrase for unlock key
var passphraseStr string
if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
Debug("Using unlock passphrase from environment variable")
passphraseStr = envPassphrase
} else {
Debug("Prompting user for unlock passphrase")
// Use secure passphrase input with confirmation
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
if err != nil {
Debug("Failed to read unlock passphrase", "error", err)
return fmt.Errorf("failed to read passphrase: %w", err)
}
}
// Create passphrase-protected unlock key
Debug("Creating passphrase-protected unlock key")
passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
if err != nil {
Debug("Failed to create unlock key", "error", err)
return fmt.Errorf("failed to create unlock key: %w", err)
}