Fix intrange and G101 linting issues

- Convert for loops to use Go 1.22+ integer ranges in generate.go and helpers.go
- Disable G101 false positives for test vectors and environment variable names
- Add file-level gosec disable for bip85_test.go containing BIP85 test vectors
- Add targeted nolint comments for legitimate test data and constants
This commit is contained in:
Jeffrey Paul 2025-06-20 08:08:01 -07:00
parent 985d79d3c0
commit 434b73d834
29 changed files with 197 additions and 280 deletions

View File

@ -6,7 +6,15 @@
"Bash(~/go/bin/govulncheck -mode=module .)", "Bash(~/go/bin/govulncheck -mode=module .)",
"Bash(go test:*)", "Bash(go test:*)",
"Bash(grep:*)", "Bash(grep:*)",
"Bash(rg:*)" "Bash(rg:*)",
"Bash(find:*)",
"Bash(make test:*)",
"Bash(go doc:*)",
"Bash(make fmt:*)",
"Bash(make:*)",
"Bash(golangci-lint run:*)",
"Bash(git add:*)",
"Bash(gofumpt:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -95,5 +95,10 @@ issues:
linters: linters:
- lll - lll
# Exclude unused parameter warnings for cobra command signatures
- text: "parameter '(args|cmd)' seems to be unused"
linters:
- revive
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0

235
TODO.md
View File

@ -1,203 +1,110 @@
# TODO for 1.0 Release # TODO for 1.0 Release
This document outlines the bugs, issues, and improvements that need to be addressed before the 1.0 release of the secret manager. This document outlines the bugs, issues, and improvements that need to be
addressed before the 1.0 release of the secret manager. Items are
prioritized from most critical (top) to least critical (bottom).
## Critical (Blockers for Release) ## Code Cleanups
### Error Handling and User Experience * we shouldn't be passing around a statedir, it should be read from the
environment or default.
- [ ] **1. Inappropriate Cobra usage printing**: Commands currently print usage information for all errors, including internal program failures. Usage should only be printed when the user provides incorrect arguments or invalid commands, not when the program encounters internal errors (like file system issues, crypto failures, etc.). ## CRITICAL SECURITY ISSUES - Must Fix Before 1.0
- [ ] **2. Inconsistent error messages**: Error messages need standardization and should be user-friendly. Many errors currently expose internal implementation details. - [ ] **1. Memory security vulnerabilities**: Sensitive data (passwords,
private keys, passphrases) stored as strings are not properly zeroed from
memory after use. Memory dumps or swap files could expose secrets. Found
in crypto.go:107, passphraseunlocker.go:29-48, cli/crypto.go:89,193,
pgpunlocker.go:278, keychainunlocker.go:252,346.
- [x] **3. Missing validation for vault names**: Vault names should be validated against a safe character set to prevent filesystem issues. ## HIGH PRIORITY SECURITY ISSUES
- [ ] **4. No graceful handling of corrupted state**: If key files are corrupted or missing, the tool should provide clear error messages and recovery suggestions. - [ ] **4. Application crashes on corrupted metadata**: Code panics instead
of returning errors when metadata is corrupt, causing denial of service.
Found in pgpunlocker.go:116 and keychainunlocker.go:141.
### Core Functionality Bugs - [ ] **5. Insufficient input validation**: Secret names allow potentially
dangerous patterns including dots that could enable path traversal attacks
(vault/secrets.go:70-93).
- [x] **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. - [ ] **6. Race conditions in file operations**: Multiple concurrent
operations could corrupt the vault state due to lack of file locking
mechanisms.
- [x] **6. Directory structure inconsistency**: The README and test script reference different directory structures: - [ ] **7. Insecure temporary file handling**: Temporary files containing
- Current code uses `unlockers.d/` but documentation shows `unlock-keys.d/` sensitive data may not be properly cleaned up or secured.
- Secret files use inconsistent naming (`secret.age` vs `value.age`)
- [x] **7. Symlink handling on non-Unix systems**: The symlink resolution in `resolveVaultSymlink()` may fail on Windows or in certain environments. ## HIGH PRIORITY FUNCTIONALITY ISSUES
- [ ] **8. Missing current unlock key initialization**: When creating vaults, no default unlock key is selected, which can cause operations to fail. - [ ] **8. Inappropriate Cobra usage printing**: Commands currently print
usage information for all errors, including internal program failures.
Usage should only be printed when the user provides incorrect arguments or
invalid commands.
- [ ] **9. Race conditions in file operations**: Multiple concurrent operations could corrupt the vault state due to lack of file locking. - [ ] **9. Missing current unlock key initialization**: When creating
vaults, no default unlock key is selected, which can cause operations to
fail.
### Security Issues - [ ] **10. Add confirmation prompts for destructive operations**:
Operations like `keys rm` and vault deletion should require confirmation.
- [ ] **10. Insecure temporary file handling**: Temporary files containing sensitive data may not be properly cleaned up or secured. - [ ] **11. No secret deletion command**: Missing `secret rm <secret-name>`
functionality.
- [ ] **11. Missing secure memory clearing**: Sensitive data in memory (passphrases, keys) should be cleared after use. - [ ] **12. Missing vault deletion command**: No way to delete vaults that
are no longer needed.
- [x] **12. Weak default permissions**: Some files may be created with overly permissive default permissions. ## MEDIUM PRIORITY ISSUES
## Important (Should be fixed before release) - [ ] **13. Inconsistent error messages**: Error messages need
standardization and should be user-friendly. Many errors currently expose
internal implementation details.
### User Interface Improvements - [ ] **14. No graceful handling of corrupted state**: If key files are
corrupted or missing, the tool should provide clear error messages and
recovery suggestions.
- [ ] **13. Add confirmation prompts for destructive operations**: Operations like `keys rm` and vault deletion should require confirmation. - [ ] **15. No validation of GPG key existence**: Should verify the
specified GPG key exists before creating PGP unlock keys.
- [ ] **14. Improve progress indicators**: Long operations (key generation, encryption) should show progress. - [ ] **16. Better separation of concerns**: Some functions in CLI do too
much and should be split.
- [x] **15. Better secret name validation**: Currently allows some characters that may cause issues, needs comprehensive validation. - [ ] **17. Environment variable security**: Sensitive data read from
environment variables (SB_UNLOCK_PASSPHRASE, SB_SECRET_MNEMONIC) without
proper clearing. Document security implications.
- [ ] **16. Add `--help` examples**: Command help should include practical examples for each operation. - [ ] **18. No secure memory allocation**: No use of mlock/munlock to
prevent sensitive data from being swapped to disk.
### Command Implementation Gaps ## LOWER PRIORITY ENHANCEMENTS
- [x] **17. `secret keys rm` not fully implemented**: Based on test output, this command may not be working correctly. - [ ] **19. Add `--help` examples**: Command help should include practical examples for each operation.
- [x] **18. `secret key select` not fully implemented**: Key selection functionality appears incomplete. - [ ] **20. Add shell completion**: Bash/Zsh completion for commands and secret names.
- [ ] **19. Missing vault deletion command**: No way to delete vaults that are no longer needed. - [ ] **21. Colored output**: Use colors to improve readability of lists and error messages.
- [ ] **20. No secret deletion command**: Missing `secret rm <secret-name>` functionality. - [ ] **22. Add `--quiet` flag**: Option to suppress non-essential output.
- [ ] **21. Missing secret history/versioning**: No way to see previous versions of secrets or restore old values. - [ ] **23. Smart secret name suggestions**: When a secret name is not found, suggest similar names.
### Configuration and Environment - [ ] **24. Audit logging**: Log all secret access and modifications for security auditing.
- [ ] **22. Global configuration not fully implemented**: The `configuration.json` file structure exists but isn't used consistently. - [ ] **25. Integration tests for hardware features**: Automated testing of Keychain and GPG functionality.
- [ ] **23. Missing environment variable validation**: Environment variables should be validated for format and security. - [ ] **26. Consistent naming conventions**: Some variables and functions use inconsistent naming patterns.
- [ ] **24. No configuration file validation**: JSON configuration files should be validated against schemas. - [ ] **27. Export/import functionality**: Add ability to export/import entire vaults, not just individual secrets.
### PGP Integration Issues - [ ] **28. Batch operations**: Add commands to process multiple secrets at once.
- [x] **25. Incomplete PGP unlock key implementation**: The `--keyid` parameter processing may not be fully working. - [ ] **29. Search functionality**: Add ability to search secret names and potentially contents.
- [ ] **26. Missing GPG agent integration**: Should detect and use existing GPG agent when available. - [ ] **30. Secret metadata**: Add support for descriptions, tags, or other metadata with secrets.
- [ ] **27. No validation of GPG key existence**: Should verify the specified GPG key exists before creating PGP unlock keys. ## COMPLETED ITEMS ✓
### Cross-Platform Issues - [x] **Missing secret history/versioning**: ✓ Implemented - versioning system exists with --version flag support
- [x] **XDG compliance on Linux**: ✓ Implemented - uses os.UserConfigDir() which respects XDG_CONFIG_HOME
- [ ] **28. macOS Keychain error handling**: Better error messages when biometric authentication fails or isn't available. - [x] **Consistent interface implementation**: ✓ Implemented - Unlocker interface is well-defined and consistently implemented
- [ ] **29. Windows path handling**: File paths may not work correctly on Windows systems.
- [ ] **30. XDG compliance on Linux**: Should respect `XDG_CONFIG_HOME` and other XDG environment variables.
## Trivial (Nice to have)
### Code Quality
- [ ] **31. Add more comprehensive unit tests**: Current test coverage could be improved, especially for error conditions.
- [ ] **32. Reduce code duplication**: Several functions have similar patterns that could be refactored.
- [ ] **33. Improve function documentation**: Many functions lack proper Go documentation comments.
- [ ] **34. Add static analysis**: Integrate tools like `staticcheck`, `golangci-lint` with more linters.
### Performance Optimizations
- [ ] **35. Cache unlock key operations**: Avoid re-reading unlock key metadata on every operation.
- [ ] **36. Optimize file I/O**: Batch file operations where possible to reduce syscalls.
- [ ] **37. Add connection pooling for HSM operations**: For hardware security module operations.
### User Experience Enhancements
- [ ] **38. Add shell completion**: Bash/Zsh completion for commands and secret names.
- [ ] **39. Colored output**: Use colors to improve readability of lists and error messages.
- [ ] **40. Add `--quiet` flag**: Option to suppress non-essential output.
- [ ] **41. Smart secret name suggestions**: When a secret name is not found, suggest similar names.
### Additional Features
- [ ] **42. Secret templates**: Predefined templates for common secret types (database URLs, API keys, etc.).
- [ ] **43. Bulk operations**: Import/export multiple secrets at once.
- [ ] **44. Secret sharing**: Secure sharing of secrets between vaults or users.
- [ ] **45. Audit logging**: Log all secret access and modifications.
- [ ] **46. Integration tests for hardware features**: Automated testing of Keychain and GPG functionality.
### Documentation
- [ ] **47. Man pages**: Generate and install proper Unix man pages.
- [ ] **48. API documentation**: Document the internal API for potential library use.
- [ ] **49. Migration guide**: Document how to migrate from other secret managers.
- [ ] **50. Security audit documentation**: Document security assumptions and threat model.
## Architecture Improvements
### Code Structure
- [ ] **51. Consistent interface implementation**: Ensure all unlocker types properly implement the Unlocker interface.
- [ ] **52. Better separation of concerns**: Some functions in CLI do too much and should be split.
- [ ] **53. Improved error types**: Create specific error types instead of using generic `fmt.Errorf`.
### Testing Infrastructure
- [x] **54. Mock filesystem consistency**: Ensure mock filesystem behavior matches real filesystem in all cases.
- [x] **55. Integration test isolation**: Tests should not affect each other or the host system.
- [ ] **56. Performance benchmarks**: Add benchmarks for crypto operations and file I/O.
## Technical Debt
- [ ] **57. Remove unused code**: Clean up any dead code or unused imports.
- [ ] **58. Standardize JSON schemas**: Create proper JSON schemas for all configuration files.
- [ ] **59. Improve error propagation**: Many functions swallow important context in error messages.
- [ ] **60. Consistent naming conventions**: Some variables and functions use inconsistent naming.
## Development Workflow
- [ ] **61. Add pre-commit hooks**: Ensure code quality and formatting before commits.
- [ ] **62. Continuous integration**: Set up CI/CD pipeline with automated testing.
- [ ] **63. Release automation**: Automate the build and release process.
- [ ] **64. Dependency management**: Regular updates and security scanning of dependencies.
---
## Priority Assessment
**Critical items** (1-12) block the 1.0 release and must be fixed for basic functionality and security.
**Important items** (13-30) should be addressed for a polished user experience but don't block the release.
**Trivial items** (31-50) are enhancements that can be addressed in future releases.
**Architecture and Infrastructure** (51-64) are longer-term improvements for maintainability and development workflow.
## Estimated Timeline
- Critical (1-12): 2-3 weeks
- Important (13-30): 3-4 weeks
- Trivial (31-50): Ongoing post-1.0
- Architecture/Infrastructure (51-64): Ongoing post-1.0
Total estimated time to 1.0: 5-7 weeks with focused development effort.
### Architecture Issues
- **Need to refactor unlock key hierarchy**: Current implementation has confusion between the top-level concepts. Fix in progress.
- Current code uses `unlockers.d/` but documentation shows `unlock-keys.d/`
- Need to settle on consistent naming: "unlock keys" vs "unlockers" throughout the codebase
- [ ] **51. Consistent interface implementation**: Ensure all unlocker types properly implement the Unlocker interface.

View File

@ -3,5 +3,5 @@ package main
import "git.eeqj.de/sneak/secret/internal/cli" import "git.eeqj.de/sneak/secret/internal/cli"
func main() { func main() {
cli.CLIEntry() cli.Entry()
} }

View File

@ -14,54 +14,54 @@ import (
) )
// Global scanner for consistent stdin reading // Global scanner for consistent stdin reading
var stdinScanner *bufio.Scanner var stdinScanner *bufio.Scanner //nolint:gochecknoglobals // Needed for consistent stdin handling
// CLIInstance encapsulates all CLI functionality and state // Instance encapsulates all CLI functionality and state
type CLIInstance struct { type Instance struct {
fs afero.Fs fs afero.Fs
stateDir string stateDir string
cmd *cobra.Command cmd *cobra.Command
} }
// NewCLIInstance creates a new CLI instance with the real filesystem // NewCLIInstance creates a new CLI instance with the real filesystem
func NewCLIInstance() *CLIInstance { func NewCLIInstance() *Instance {
fs := afero.NewOsFs() fs := afero.NewOsFs()
stateDir := secret.DetermineStateDir("") stateDir := secret.DetermineStateDir("")
return &CLIInstance{ return &Instance{
fs: fs, fs: fs,
stateDir: stateDir, stateDir: stateDir,
} }
} }
// NewCLIInstanceWithFs creates a new CLI instance with the given filesystem (for testing) // NewCLIInstanceWithFs creates a new CLI instance with the given filesystem (for testing)
func NewCLIInstanceWithFs(fs afero.Fs) *CLIInstance { func NewCLIInstanceWithFs(fs afero.Fs) *Instance {
stateDir := secret.DetermineStateDir("") stateDir := secret.DetermineStateDir("")
return &CLIInstance{ return &Instance{
fs: fs, fs: fs,
stateDir: stateDir, stateDir: stateDir,
} }
} }
// NewCLIInstanceWithStateDir creates a new CLI instance with custom state directory (for testing) // NewCLIInstanceWithStateDir creates a new CLI instance with custom state directory (for testing)
func NewCLIInstanceWithStateDir(fs afero.Fs, stateDir string) *CLIInstance { func NewCLIInstanceWithStateDir(fs afero.Fs, stateDir string) *Instance {
return &CLIInstance{ return &Instance{
fs: fs, fs: fs,
stateDir: stateDir, stateDir: stateDir,
} }
} }
// SetFilesystem sets the filesystem for this CLI instance (for testing) // SetFilesystem sets the filesystem for this CLI instance (for testing)
func (cli *CLIInstance) SetFilesystem(fs afero.Fs) { func (cli *Instance) SetFilesystem(fs afero.Fs) {
cli.fs = fs cli.fs = fs
} }
// SetStateDir sets the state directory for this CLI instance (for testing) // SetStateDir sets the state directory for this CLI instance (for testing)
func (cli *CLIInstance) SetStateDir(stateDir string) { func (cli *Instance) SetStateDir(stateDir string) {
cli.stateDir = stateDir cli.stateDir = stateDir
} }
// GetStateDir returns the state directory for this CLI instance // GetStateDir returns the state directory for this CLI instance
func (cli *CLIInstance) GetStateDir() string { func (cli *Instance) GetStateDir() string {
return cli.stateDir return cli.stateDir
} }
@ -77,7 +77,7 @@ func getStdinScanner() *bufio.Scanner {
// Uses a shared scanner to avoid buffering issues between multiple calls // Uses a shared scanner to avoid buffering issues between multiple calls
func readLineFromStdin(prompt string) (string, error) { func readLineFromStdin(prompt string) (string, error) {
// Check if stderr is a terminal - if not, we can't prompt interactively // Check if stderr is a terminal - if not, we can't prompt interactively
if !term.IsTerminal(int(syscall.Stderr)) { if !term.IsTerminal(syscall.Stderr) {
return "", fmt.Errorf("cannot prompt for input: stderr is not a terminal (running in non-interactive mode)") return "", fmt.Errorf("cannot prompt for input: stderr is not a terminal (running in non-interactive mode)")
} }

View File

@ -54,7 +54,7 @@ func newDecryptCmd() *cobra.Command {
} }
// Encrypt encrypts data using an age secret key stored in a secret // Encrypt encrypts data using an age secret key stored in a secret
func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error { func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {
@ -157,7 +157,7 @@ func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error
} }
// Decrypt decrypts data using an age secret key stored in a secret // Decrypt decrypts data using an age secret key stored in a secret
func (cli *CLIInstance) Decrypt(secretName, inputFile, outputFile string) error { func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {

View File

@ -60,7 +60,7 @@ func newGenerateSecretCmd() *cobra.Command {
} }
// GenerateMnemonic generates a random BIP39 mnemonic phrase // GenerateMnemonic generates a random BIP39 mnemonic phrase
func (cli *CLIInstance) GenerateMnemonic(cmd *cobra.Command) error { func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error {
// Generate 128 bits of entropy for a 12-word mnemonic // Generate 128 bits of entropy for a 12-word mnemonic
entropy, err := bip39.NewEntropy(128) entropy, err := bip39.NewEntropy(128)
if err != nil { if err != nil {
@ -92,7 +92,7 @@ func (cli *CLIInstance) GenerateMnemonic(cmd *cobra.Command) error {
} }
// GenerateSecret generates a random secret and stores it in the vault // GenerateSecret generates a random secret and stores it in the vault
func (cli *CLIInstance) GenerateSecret(cmd *cobra.Command, secretName string, length int, secretType string, force bool) error { func (cli *Instance) GenerateSecret(cmd *cobra.Command, secretName string, length int, secretType string, force bool) error {
if length < 1 { if length < 1 {
return fmt.Errorf("length must be at least 1") return fmt.Errorf("length must be at least 1")
} }
@ -150,7 +150,7 @@ func generateRandomString(length int, charset string) (string, error) {
result := make([]byte, length) result := make([]byte, length)
charsetLen := big.NewInt(int64(len(charset))) charsetLen := big.NewInt(int64(len(charset)))
for i := 0; i < length; i++ { for i := range length {
randomIndex, err := rand.Int(rand.Reader, charsetLen) randomIndex, err := rand.Int(rand.Reader, charsetLen)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err) return "", fmt.Errorf("failed to generate random number: %w", err)

View File

@ -33,7 +33,7 @@ func RunInit(cmd *cobra.Command, args []string) error {
} }
// Init initializes the secret manager // Init initializes the secret manager
func (cli *CLIInstance) Init(cmd *cobra.Command) error { func (cli *Instance) Init(cmd *cobra.Command) error {
secret.Debug("Starting secret manager initialization") secret.Debug("Starting secret manager initialization")
// Create state directory // Create state directory

View File

@ -1387,7 +1387,7 @@ func test19DisasterRecovery(t *testing.T, tempDir, secretPath, testMnemonic stri
// Write the long-term private key to a file for age CLI // Write the long-term private key to a file for age CLI
ltPrivKeyPath := filepath.Join(tempDir, "lt-private.key") ltPrivKeyPath := filepath.Join(tempDir, "lt-private.key")
err = os.WriteFile(ltPrivKeyPath, []byte(ltIdentity.String()), 0600) err = os.WriteFile(ltPrivKeyPath, []byte(ltIdentity.String()), 0o600)
require.NoError(t, err, "write long-term private key") require.NoError(t, err, "write long-term private key")
// Find the secret version directory // Find the secret version directory
@ -1606,7 +1606,7 @@ func test23ErrorHandling(t *testing.T, tempDir, secretPath, testMnemonic string,
func test24EnvironmentVariables(t *testing.T, tempDir, secretPath, testMnemonic, testPassphrase string) { func test24EnvironmentVariables(t *testing.T, tempDir, secretPath, testMnemonic, testPassphrase string) {
// Create a new temporary directory for this test // Create a new temporary directory for this test
envTestDir := filepath.Join(tempDir, "env-test") envTestDir := filepath.Join(tempDir, "env-test")
err := os.MkdirAll(envTestDir, 0700) err := os.MkdirAll(envTestDir, 0o700)
require.NoError(t, err, "create env test dir should succeed") require.NoError(t, err, "create env test dir should succeed")
// Test init with both env vars set // Test init with both env vars set
@ -1908,7 +1908,7 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string,
// Create backup directory // Create backup directory
backupDir := filepath.Join(tempDir, "backup") backupDir := filepath.Join(tempDir, "backup")
err := os.MkdirAll(backupDir, 0700) err := os.MkdirAll(backupDir, 0o700)
require.NoError(t, err, "create backup dir should succeed") require.NoError(t, err, "create backup dir should succeed")
// Copy entire state directory to backup // Copy entire state directory to backup
@ -2012,7 +2012,7 @@ func test31EnvMnemonicUsesVaultDerivationIndex(t *testing.T, tempDir, secretPath
require.NoError(t, err, "vault select work should succeed") require.NoError(t, err, "vault select work should succeed")
// Add a secret to work vault using environment mnemonic // Add a secret to work vault using environment mnemonic
secretValue := "work-vault-secret" secretValue := "work-vault-secret" //nolint:gosec // G101: This is test data, not a real credential
cmd := exec.Command(secretPath, "add", "test/derivation") cmd := exec.Command(secretPath, "add", "test/derivation")
cmd.Env = []string{ cmd.Env = []string{
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir), fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
@ -2083,7 +2083,7 @@ func readFile(t *testing.T, path string) []byte {
// writeFile writes data to a file // writeFile writes data to a file
func writeFile(t *testing.T, path string, data []byte) { func writeFile(t *testing.T, path string, data []byte) {
t.Helper() t.Helper()
err := os.WriteFile(path, data, 0600) err := os.WriteFile(path, data, 0o600)
require.NoError(t, err, "Should be able to write file: %s", path) require.NoError(t, err, "Should be able to write file: %s", path)
} }
@ -2094,7 +2094,7 @@ func copyDir(src, dst string) error {
return err return err
} }
err = os.MkdirAll(dst, 0755) err = os.MkdirAll(dst, 0o755)
if err != nil { if err != nil {
return err return err
} }
@ -2146,7 +2146,7 @@ func copyFile(src, dst string) error {
return err return err
} }
err = os.WriteFile(dst, srcData, 0644) err = os.WriteFile(dst, srcData, 0o644)
if err != nil { if err != nil {
return err return err
} }

View File

@ -7,8 +7,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// CLIEntry is the entry point for the secret CLI application // Entry is the entry point for the secret CLI application
func CLIEntry() { func Entry() {
cmd := newRootCmd() cmd := newRootCmd()
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)

View File

@ -96,7 +96,7 @@ func newImportCmd() *cobra.Command {
} }
// AddSecret adds a secret to the current vault // AddSecret adds a secret to the current vault
func (cli *CLIInstance) AddSecret(secretName string, force bool) error { func (cli *Instance) AddSecret(secretName string, force bool) error {
secret.Debug("CLI AddSecret starting", "secret_name", secretName, "force", force) secret.Debug("CLI AddSecret starting", "secret_name", secretName, "force", force)
// Get current vault // Get current vault
@ -135,12 +135,12 @@ func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
} }
// GetSecret retrieves and prints a secret from the current vault // GetSecret retrieves and prints a secret from the current vault
func (cli *CLIInstance) GetSecret(cmd *cobra.Command, secretName string) error { func (cli *Instance) GetSecret(cmd *cobra.Command, secretName string) error {
return cli.GetSecretWithVersion(cmd, secretName, "") return cli.GetSecretWithVersion(cmd, secretName, "")
} }
// GetSecretWithVersion retrieves and prints a specific version of a secret // GetSecretWithVersion retrieves and prints a specific version of a secret
func (cli *CLIInstance) GetSecretWithVersion(cmd *cobra.Command, secretName string, version string) error { func (cli *Instance) GetSecretWithVersion(cmd *cobra.Command, secretName string, version string) error {
secret.Debug("GetSecretWithVersion called", "secretName", secretName, "version", version) secret.Debug("GetSecretWithVersion called", "secretName", secretName, "version", version)
// Get current vault // Get current vault
@ -180,7 +180,7 @@ func (cli *CLIInstance) GetSecretWithVersion(cmd *cobra.Command, secretName stri
} }
// ListSecrets lists all secrets in the current vault // ListSecrets lists all secrets in the current vault
func (cli *CLIInstance) ListSecrets(cmd *cobra.Command, jsonOutput bool, filter string) error { func (cli *Instance) ListSecrets(cmd *cobra.Command, jsonOutput bool, filter string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {
@ -278,7 +278,7 @@ func (cli *CLIInstance) ListSecrets(cmd *cobra.Command, jsonOutput bool, filter
} }
// ImportSecret imports a secret from a file // ImportSecret imports a secret from a file
func (cli *CLIInstance) ImportSecret(cmd *cobra.Command, secretName, sourceFile string, force bool) error { func (cli *Instance) ImportSecret(cmd *cobra.Command, secretName, sourceFile string, force bool) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {

View File

@ -102,7 +102,7 @@ func newUnlockerSelectSubCmd() *cobra.Command {
} }
// UnlockersList lists unlockers in the current vault // UnlockersList lists unlockers in the current vault
func (cli *CLIInstance) UnlockersList(jsonOutput bool) error { func (cli *Instance) UnlockersList(jsonOutput bool) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {
@ -150,12 +150,12 @@ func (cli *CLIInstance) UnlockersList(jsonOutput bool) error {
// Check if this is the right unlocker by comparing metadata // Check if this is the right unlocker by comparing metadata
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath) metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
if err != nil { if err != nil {
continue //FIXME this error needs to be handled continue // FIXME this error needs to be handled
} }
var diskMetadata secret.UnlockerMetadata var diskMetadata secret.UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil { if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
continue //FIXME this error needs to be handled continue // FIXME this error needs to be handled
} }
// Match by type and creation time // Match by type and creation time
@ -233,7 +233,7 @@ func (cli *CLIInstance) UnlockersList(jsonOutput bool) error {
} }
// UnlockersAdd adds a new unlocker // UnlockersAdd adds a new unlocker
func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error { func (cli *Instance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
switch unlockerType { switch unlockerType {
case "passphrase": case "passphrase":
// Get current vault // Get current vault
@ -303,7 +303,7 @@ func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) er
} }
// UnlockersRemove removes an unlocker // UnlockersRemove removes an unlocker
func (cli *CLIInstance) UnlockersRemove(unlockerID string) error { func (cli *Instance) UnlockersRemove(unlockerID string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {
@ -314,7 +314,7 @@ func (cli *CLIInstance) UnlockersRemove(unlockerID string) error {
} }
// UnlockerSelect selects an unlocker as current // UnlockerSelect selects an unlocker as current
func (cli *CLIInstance) UnlockerSelect(unlockerID string) error { func (cli *Instance) UnlockerSelect(unlockerID string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {

View File

@ -89,7 +89,7 @@ func newVaultImportCmd() *cobra.Command {
} }
// ListVaults lists all available vaults // ListVaults lists all available vaults
func (cli *CLIInstance) ListVaults(cmd *cobra.Command, jsonOutput bool) error { func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
vaults, err := vault.ListVaults(cli.fs, cli.stateDir) vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
if err != nil { if err != nil {
return err return err
@ -138,7 +138,7 @@ func (cli *CLIInstance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
} }
// CreateVault creates a new vault // CreateVault creates a new vault
func (cli *CLIInstance) CreateVault(cmd *cobra.Command, name string) error { func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir) secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name) vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name)
@ -151,7 +151,7 @@ func (cli *CLIInstance) CreateVault(cmd *cobra.Command, name string) error {
} }
// SelectVault selects a vault as the current one // SelectVault selects a vault as the current one
func (cli *CLIInstance) SelectVault(cmd *cobra.Command, name string) error { func (cli *Instance) SelectVault(cmd *cobra.Command, name string) error {
if err := vault.SelectVault(cli.fs, cli.stateDir, name); err != nil { if err := vault.SelectVault(cli.fs, cli.stateDir, name); err != nil {
return err return err
} }
@ -161,7 +161,7 @@ func (cli *CLIInstance) SelectVault(cmd *cobra.Command, name string) error {
} }
// VaultImport imports a mnemonic into a specific vault // VaultImport imports a mnemonic into a specific vault
func (cli *CLIInstance) VaultImport(cmd *cobra.Command, vaultName string) error { func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
secret.Debug("Importing mnemonic into vault", "vault_name", vaultName, "state_dir", cli.stateDir) secret.Debug("Importing mnemonic into vault", "vault_name", vaultName, "state_dir", cli.stateDir)
// Get the specific vault by name // Get the specific vault by name
@ -219,7 +219,7 @@ func (cli *CLIInstance) VaultImport(cmd *cobra.Command, vaultName string) error
ltPublicKey := ltIdentity.Recipient().String() ltPublicKey := ltIdentity.Recipient().String()
secret.Debug("Storing long-term public key", "pubkey", ltPublicKey, "vault_dir", vaultDir) secret.Debug("Storing long-term public key", "pubkey", ltPublicKey, "vault_dir", vaultDir)
if err := afero.WriteFile(cli.fs, pubKeyPath, []byte(ltPublicKey), 0600); err != nil { if err := afero.WriteFile(cli.fs, pubKeyPath, []byte(ltPublicKey), 0o600); err != nil {
return fmt.Errorf("failed to store long-term public key: %w", err) return fmt.Errorf("failed to store long-term public key: %w", err)
} }

View File

@ -19,7 +19,7 @@ func newVersionCmd() *cobra.Command {
} }
// VersionCommands returns the version management commands // VersionCommands returns the version management commands
func VersionCommands(cli *CLIInstance) *cobra.Command { func VersionCommands(cli *Instance) *cobra.Command {
versionCmd := &cobra.Command{ versionCmd := &cobra.Command{
Use: "version", Use: "version",
Short: "Manage secret versions", Short: "Manage secret versions",
@ -52,7 +52,7 @@ func VersionCommands(cli *CLIInstance) *cobra.Command {
} }
// ListVersions lists all versions of a secret // ListVersions lists all versions of a secret
func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) error { func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
secret.Debug("ListVersions called", "secret_name", secretName) secret.Debug("ListVersions called", "secret_name", secretName)
// Get current vault // Get current vault
@ -158,7 +158,7 @@ func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) erro
} }
// PromoteVersion promotes a specific version to current // PromoteVersion promotes a specific version to current
func (cli *CLIInstance) PromoteVersion(cmd *cobra.Command, secretName string, version string) error { func (cli *Instance) PromoteVersion(cmd *cobra.Command, secretName string, version string) error {
// Get current vault // Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil { if err != nil {

View File

@ -18,12 +18,11 @@ package cli
import ( import (
"bytes" "bytes"
"path/filepath"
"strings" "strings"
"testing" "testing"
"time" "time"
"path/filepath"
"git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/internal/vault"
"git.eeqj.de/sneak/secret/pkg/agehd" "git.eeqj.de/sneak/secret/pkg/agehd"
@ -49,7 +48,7 @@ func setupTestVault(t *testing.T, fs afero.Fs, stateDir string) {
// Store long-term public key in vault // Store long-term public key in vault
vaultDir, _ := vlt.GetDirectory() vaultDir, _ := vlt.GetDirectory()
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Select vault // Select vault
@ -289,7 +288,7 @@ func TestListVersionsEmptyOutput(t *testing.T) {
// Create a secret directory without versions (edge case) // Create a secret directory without versions (edge case)
vaultDir := stateDir + "/vaults.d/default" vaultDir := stateDir + "/vaults.d/default"
secretDir := vaultDir + "/secrets.d/test%secret" secretDir := vaultDir + "/secrets.d/test%secret"
err := fs.MkdirAll(secretDir, 0755) err := fs.MkdirAll(secretDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
// Create a command for output capture // Create a command for output capture

View File

@ -9,15 +9,15 @@ const (
// Environment variable names // Environment variable names
EnvStateDir = "SB_SECRET_STATE_DIR" EnvStateDir = "SB_SECRET_STATE_DIR"
EnvMnemonic = "SB_SECRET_MNEMONIC" EnvMnemonic = "SB_SECRET_MNEMONIC"
EnvUnlockPassphrase = "SB_UNLOCK_PASSPHRASE" EnvUnlockPassphrase = "SB_UNLOCK_PASSPHRASE" //nolint:gosec // G101: This is an env var name, not a credential
EnvGPGKeyID = "SB_GPG_KEY_ID" EnvGPGKeyID = "SB_GPG_KEY_ID"
) )
// File system permission constants // File system permission constants
const ( const (
// DirPerms is the permission used for directories (read-write-execute for owner only) // DirPerms is the permission used for directories (read-write-execute for owner only)
DirPerms os.FileMode = 0700 DirPerms os.FileMode = 0o700
// FilePerms is the permission used for sensitive files (read-write for owner only) // FilePerms is the permission used for sensitive files (read-write for owner only)
FilePerms os.FileMode = 0600 FilePerms os.FileMode = 0o600
) )

View File

@ -17,7 +17,7 @@ func generateRandomString(length int, charset string) (string, error) {
result := make([]byte, length) result := make([]byte, length)
charsetLen := big.NewInt(int64(len(charset))) charsetLen := big.NewInt(int64(len(charset)))
for i := 0; i < length; i++ { for i := range length {
randomIndex, err := rand.Int(rand.Reader, charsetLen) randomIndex, err := rand.Int(rand.Reader, charsetLen)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err) return "", fmt.Errorf("failed to generate random number: %w", err)

View File

@ -16,11 +16,9 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
var ( // keychainItemNameRegex validates keychain item names
// keychainItemNameRegex validates keychain item names // Allows alphanumeric characters, dots, hyphens, and underscores only
// Allows alphanumeric characters, dots, hyphens, and underscores only var keychainItemNameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
keychainItemNameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
)
// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data // KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
type KeychainUnlockerMetadata struct { type KeychainUnlockerMetadata struct {

View File

@ -35,7 +35,7 @@ func setupNonInteractiveGPG(t *testing.T, tempDir, passphrase, gnupgHomeDir stri
no-tty no-tty
pinentry-mode loopback pinentry-mode loopback
` `
if err := os.WriteFile(gpgConfPath, []byte(gpgConfContent), 0600); err != nil { if err := os.WriteFile(gpgConfPath, []byte(gpgConfContent), 0o600); err != nil {
t.Fatalf("Failed to write GPG config file: %v", err) t.Fatalf("Failed to write GPG config file: %v", err)
} }
@ -139,7 +139,7 @@ func TestPGPUnlockerWithRealFS(t *testing.T) {
// Create a temporary GNUPGHOME // Create a temporary GNUPGHOME
gnupgHomeDir := filepath.Join(tempDir, "gnupg") gnupgHomeDir := filepath.Join(tempDir, "gnupg")
if err := os.MkdirAll(gnupgHomeDir, 0700); err != nil { if err := os.MkdirAll(gnupgHomeDir, 0o700); err != nil {
t.Fatalf("Failed to create GNUPGHOME: %v", err) t.Fatalf("Failed to create GNUPGHOME: %v", err)
} }
@ -176,7 +176,7 @@ Passphrase: ` + testPassphrase + `
%commit %commit
%echo Key generation completed %echo Key generation completed
` `
if err := os.WriteFile(batchFile, []byte(batchContent), 0600); err != nil { if err := os.WriteFile(batchFile, []byte(batchContent), 0o600); err != nil {
t.Fatalf("Failed to write batch file: %v", err) t.Fatalf("Failed to write batch file: %v", err)
} }

View File

@ -29,14 +29,14 @@ func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
// Create secret directory with proper storage name conversion // Create secret directory with proper storage name conversion
storageName := strings.ReplaceAll(name, "/", "%") storageName := strings.ReplaceAll(name, "/", "%")
secretDir := filepath.Join(m.directory, "secrets.d", storageName) secretDir := filepath.Join(m.directory, "secrets.d", storageName)
if err := m.fs.MkdirAll(secretDir, 0700); err != nil { if err := m.fs.MkdirAll(secretDir, 0o700); err != nil {
return err return err
} }
// Create version directory with proper path // Create version directory with proper path
versionName := "20240101.001" // Use a fixed version name for testing versionName := "20240101.001" // Use a fixed version name for testing
versionDir := filepath.Join(secretDir, "versions", versionName) versionDir := filepath.Join(secretDir, "versions", versionName)
if err := m.fs.MkdirAll(versionDir, 0700); err != nil { if err := m.fs.MkdirAll(versionDir, 0o700); err != nil {
return err return err
} }
@ -57,7 +57,7 @@ func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
// Write long-term public key if it doesn't exist // Write long-term public key if it doesn't exist
if _, err := m.fs.Stat(ltPubKeyPath); os.IsNotExist(err) { if _, err := m.fs.Stat(ltPubKeyPath); os.IsNotExist(err) {
pubKey := ltIdentity.Recipient().String() pubKey := ltIdentity.Recipient().String()
if err := afero.WriteFile(m.fs, ltPubKeyPath, []byte(pubKey), 0600); err != nil { if err := afero.WriteFile(m.fs, ltPubKeyPath, []byte(pubKey), 0o600); err != nil {
return err return err
} }
} }
@ -70,7 +70,7 @@ func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
// Write version public key // Write version public key
pubKeyPath := filepath.Join(versionDir, "pub.age") pubKeyPath := filepath.Join(versionDir, "pub.age")
if err := afero.WriteFile(m.fs, pubKeyPath, []byte(versionIdentity.Recipient().String()), 0600); err != nil { if err := afero.WriteFile(m.fs, pubKeyPath, []byte(versionIdentity.Recipient().String()), 0o600); err != nil {
return err return err
} }
@ -82,7 +82,7 @@ func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
// Write encrypted value // Write encrypted value
valuePath := filepath.Join(versionDir, "value.age") valuePath := filepath.Join(versionDir, "value.age")
if err := afero.WriteFile(m.fs, valuePath, encryptedValue, 0600); err != nil { if err := afero.WriteFile(m.fs, valuePath, encryptedValue, 0o600); err != nil {
return err return err
} }
@ -94,14 +94,14 @@ func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
// Write encrypted version private key // Write encrypted version private key
privKeyPath := filepath.Join(versionDir, "priv.age") privKeyPath := filepath.Join(versionDir, "priv.age")
if err := afero.WriteFile(m.fs, privKeyPath, encryptedPrivKey, 0600); err != nil { if err := afero.WriteFile(m.fs, privKeyPath, encryptedPrivKey, 0o600); err != nil {
return err return err
} }
// Create current symlink pointing to the version // Create current symlink pointing to the version
currentLink := filepath.Join(secretDir, "current") currentLink := filepath.Join(secretDir, "current")
// For MemMapFs, write a file with the target path // For MemMapFs, write a file with the target path
if err := afero.WriteFile(m.fs, currentLink, []byte("versions/"+versionName), 0600); err != nil { if err := afero.WriteFile(m.fs, currentLink, []byte("versions/"+versionName), 0o600); err != nil {
return err return err
} }
@ -164,7 +164,7 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
fs, fs,
ltPubKeyPath, ltPubKeyPath,
[]byte(ltIdentity.Recipient().String()), []byte(ltIdentity.Recipient().String()),
0600, 0o600,
) )
if err != nil { if err != nil {
t.Fatalf("Failed to write long-term public key: %v", err) t.Fatalf("Failed to write long-term public key: %v", err)
@ -325,7 +325,7 @@ func TestSecretGetValueWithEnvMnemonicUsesVaultDerivationIndex(t *testing.T) {
}() }()
stateDir := filepath.Join(tempDir, ".secret") stateDir := filepath.Join(tempDir, ".secret")
require.NoError(t, fs.MkdirAll(stateDir, 0700)) require.NoError(t, fs.MkdirAll(stateDir, 0o700))
// This test is now in the integration test file where it can use real vaults // This test is now in the integration test file where it can use real vaults
// The bug is demonstrated there - see test31EnvMnemonicUsesVaultDerivationIndex // The bug is demonstrated there - see test31EnvMnemonicUsesVaultDerivationIndex

View File

@ -89,7 +89,7 @@ func TestGenerateVersionName(t *testing.T) {
// Create the version directory // Create the version directory
versionDir := filepath.Join(secretDir, "versions", version1) versionDir := filepath.Join(secretDir, "versions", version1)
err = fs.MkdirAll(versionDir, 0755) err = fs.MkdirAll(versionDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
// Test second version generation on same day // Test second version generation on same day
@ -111,7 +111,7 @@ func TestGenerateVersionNameMaxSerial(t *testing.T) {
today := time.Now().Format("20060102") today := time.Now().Format("20060102")
for i := 1; i <= 999; i++ { for i := 1; i <= 999; i++ {
versionName := fmt.Sprintf("%s.%03d", today, i) versionName := fmt.Sprintf("%s.%03d", today, i)
err := fs.MkdirAll(filepath.Join(versionsDir, versionName), 0755) err := fs.MkdirAll(filepath.Join(versionsDir, versionName), 0o755)
require.NoError(t, err) require.NoError(t, err)
} }
@ -148,7 +148,7 @@ func TestSecretVersionSave(t *testing.T) {
// Create vault directory structure and long-term key // Create vault directory structure and long-term key
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
err := fs.MkdirAll(vaultDir, 0755) err := fs.MkdirAll(vaultDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
// Generate and store long-term public key // Generate and store long-term public key
@ -157,7 +157,7 @@ func TestSecretVersionSave(t *testing.T) {
vault.longTermKey = ltIdentity vault.longTermKey = ltIdentity
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Create and save a version // Create and save a version
@ -184,7 +184,7 @@ func TestSecretVersionLoadMetadata(t *testing.T) {
// Setup vault with long-term key // Setup vault with long-term key
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
err := fs.MkdirAll(vaultDir, 0755) err := fs.MkdirAll(vaultDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
ltIdentity, err := age.GenerateX25519Identity() ltIdentity, err := age.GenerateX25519Identity()
@ -192,7 +192,7 @@ func TestSecretVersionLoadMetadata(t *testing.T) {
vault.longTermKey = ltIdentity vault.longTermKey = ltIdentity
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Create and save a version with custom metadata // Create and save a version with custom metadata
@ -227,7 +227,7 @@ func TestSecretVersionGetValue(t *testing.T) {
// Setup vault with long-term key // Setup vault with long-term key
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
err := fs.MkdirAll(vaultDir, 0755) err := fs.MkdirAll(vaultDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
ltIdentity, err := age.GenerateX25519Identity() ltIdentity, err := age.GenerateX25519Identity()
@ -235,7 +235,7 @@ func TestSecretVersionGetValue(t *testing.T) {
vault.longTermKey = ltIdentity vault.longTermKey = ltIdentity
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Create and save a version // Create and save a version
@ -265,12 +265,12 @@ func TestListVersions(t *testing.T) {
// Create some versions // Create some versions
testVersions := []string{"20231215.001", "20231215.002", "20231216.001", "20231214.001"} testVersions := []string{"20231215.001", "20231215.002", "20231216.001", "20231214.001"}
for _, v := range testVersions { for _, v := range testVersions {
err := fs.MkdirAll(filepath.Join(versionsDir, v), 0755) err := fs.MkdirAll(filepath.Join(versionsDir, v), 0o755)
require.NoError(t, err) require.NoError(t, err)
} }
// Create a file (not directory) that should be ignored // Create a file (not directory) that should be ignored
err = afero.WriteFile(fs, filepath.Join(versionsDir, "ignore.txt"), []byte("test"), 0600) err = afero.WriteFile(fs, filepath.Join(versionsDir, "ignore.txt"), []byte("test"), 0o600)
require.NoError(t, err) require.NoError(t, err)
// List versions // List versions
@ -288,10 +288,10 @@ func TestGetCurrentVersion(t *testing.T) {
// Simulate symlink with file content (works for both OsFs and MemMapFs) // Simulate symlink with file content (works for both OsFs and MemMapFs)
currentPath := filepath.Join(secretDir, "current") currentPath := filepath.Join(secretDir, "current")
err := fs.MkdirAll(secretDir, 0755) err := fs.MkdirAll(secretDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
err = afero.WriteFile(fs, currentPath, []byte("versions/20231216.001"), 0600) err = afero.WriteFile(fs, currentPath, []byte("versions/20231216.001"), 0o600)
require.NoError(t, err) require.NoError(t, err)
version, err := GetCurrentVersion(fs, secretDir) version, err := GetCurrentVersion(fs, secretDir)
@ -303,7 +303,7 @@ func TestSetCurrentVersion(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
secretDir := "/test/secret" secretDir := "/test/secret"
err := fs.MkdirAll(secretDir, 0755) err := fs.MkdirAll(secretDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
// Set current version // Set current version

View File

@ -51,7 +51,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test symlink handling // Test symlink handling
t.Run("SymlinkHandling", func(t *testing.T) { t.Run("SymlinkHandling", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "symlink-test") stateDir := filepath.Join(tempDir, "symlink-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }
@ -98,7 +98,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test secret operations with deeply nested paths // Test secret operations with deeply nested paths
t.Run("DeepPathSecrets", func(t *testing.T) { t.Run("DeepPathSecrets", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "deep-path-test") stateDir := filepath.Join(tempDir, "deep-path-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }
@ -169,7 +169,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test key caching in GetOrDeriveLongTermKey // Test key caching in GetOrDeriveLongTermKey
t.Run("KeyCaching", func(t *testing.T) { t.Run("KeyCaching", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "key-cache-test") stateDir := filepath.Join(tempDir, "key-cache-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }
@ -251,7 +251,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test vault name validation // Test vault name validation
t.Run("VaultNameValidation", func(t *testing.T) { t.Run("VaultNameValidation", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "name-validation-test") stateDir := filepath.Join(tempDir, "name-validation-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }
@ -291,7 +291,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test multiple vaults and switching between them // Test multiple vaults and switching between them
t.Run("MultipleVaults", func(t *testing.T) { t.Run("MultipleVaults", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "multi-vault-test") stateDir := filepath.Join(tempDir, "multi-vault-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }
@ -336,7 +336,7 @@ func TestVaultWithRealFilesystem(t *testing.T) {
// Test adding a secret in one vault and verifying it's not visible in another // Test adding a secret in one vault and verifying it's not visible in another
t.Run("VaultIsolation", func(t *testing.T) { t.Run("VaultIsolation", func(t *testing.T) {
stateDir := filepath.Join(tempDir, "isolation-test") stateDir := filepath.Join(tempDir, "isolation-test")
if err := os.MkdirAll(stateDir, 0700); err != nil { if err := os.MkdirAll(stateDir, 0o700); err != nil {
t.Fatalf("Failed to create state dir: %v", err) t.Fatalf("Failed to create state dir: %v", err)
} }

View File

@ -54,7 +54,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
// Store long-term public key in vault // Store long-term public key in vault
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Unlock the vault // Unlock the vault
@ -222,7 +222,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
for i := 2; i <= 998; i++ { for i := 2; i <= 998; i++ {
versionName := fmt.Sprintf("%s.%03d", today, i) versionName := fmt.Sprintf("%s.%03d", today, i)
versionDir := filepath.Join(secretDir, versionName) versionDir := filepath.Join(secretDir, versionName)
err := fs.MkdirAll(versionDir, 0755) err := fs.MkdirAll(versionDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
} }
@ -232,7 +232,7 @@ func TestVersionIntegrationWorkflow(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%s.999", today), versionName) assert.Equal(t, fmt.Sprintf("%s.999", today), versionName)
// Create the 999th version directory // Create the 999th version directory
err = fs.MkdirAll(filepath.Join(secretDir, versionName), 0755) err = fs.MkdirAll(filepath.Join(secretDir, versionName), 0o755)
require.NoError(t, err) require.NoError(t, err)
// Should fail to create 1000th version // Should fail to create 1000th version
@ -319,7 +319,7 @@ func TestVersionCompatibility(t *testing.T) {
secretName := "legacy/secret" secretName := "legacy/secret"
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
secretDir := filepath.Join(vaultDir, "secrets.d", "legacy%secret") secretDir := filepath.Join(vaultDir, "secrets.d", "legacy%secret")
err = fs.MkdirAll(secretDir, 0755) err = fs.MkdirAll(secretDir, 0o755)
require.NoError(t, err) require.NoError(t, err)
// Create old-style encrypted value directly in secret directory // Create old-style encrypted value directly in secret directory
@ -329,7 +329,7 @@ func TestVersionCompatibility(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
valuePath := filepath.Join(secretDir, "value.age") valuePath := filepath.Join(secretDir, "value.age")
err = afero.WriteFile(fs, valuePath, encrypted, 0600) err = afero.WriteFile(fs, valuePath, encrypted, 0o600)
require.NoError(t, err) require.NoError(t, err)
// Should fail to get with version-aware methods // Should fail to get with version-aware methods

View File

@ -13,10 +13,12 @@ import (
) )
// Alias the metadata types from secret package for convenience // Alias the metadata types from secret package for convenience
type VaultMetadata = secret.VaultMetadata type (
type UnlockerMetadata = secret.UnlockerMetadata VaultMetadata = secret.VaultMetadata
type SecretMetadata = secret.SecretMetadata UnlockerMetadata = secret.UnlockerMetadata
type Configuration = secret.Configuration SecretMetadata = secret.SecretMetadata
Configuration = secret.Configuration
)
// ComputeDoubleSHA256 computes the double SHA256 hash of data and returns it as hex // ComputeDoubleSHA256 computes the double SHA256 hash of data and returns it as hex
func ComputeDoubleSHA256(data []byte) string { func ComputeDoubleSHA256(data []byte) string {

View File

@ -1,11 +1,9 @@
package vault package vault
import ( import (
"testing"
"path/filepath" "path/filepath"
"strings" "strings"
"testing"
"git.eeqj.de/sneak/secret/pkg/agehd" "git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -53,7 +51,7 @@ func TestVaultMetadata(t *testing.T) {
// Create a vault with metadata and matching public key // Create a vault with metadata and matching public key
vaultDir := filepath.Join(stateDir, "vaults.d", "vault1") vaultDir := filepath.Join(stateDir, "vaults.d", "vault1")
if err := fs.MkdirAll(vaultDir, 0700); err != nil { if err := fs.MkdirAll(vaultDir, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err) t.Fatalf("Failed to create vault directory: %v", err)
} }
@ -66,7 +64,7 @@ func TestVaultMetadata(t *testing.T) {
pubKeyHash0 := ComputeDoubleSHA256([]byte(pubKey0)) pubKeyHash0 := ComputeDoubleSHA256([]byte(pubKey0))
// Write public key // Write public key
if err := afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(pubKey0), 0600); err != nil { if err := afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(pubKey0), 0o600); err != nil {
t.Fatalf("Failed to write public key: %v", err) t.Fatalf("Failed to write public key: %v", err)
} }
@ -100,7 +98,7 @@ func TestVaultMetadata(t *testing.T) {
// Add another vault with same mnemonic but higher index // Add another vault with same mnemonic but higher index
vaultDir2 := filepath.Join(stateDir, "vaults.d", "vault2") vaultDir2 := filepath.Join(stateDir, "vaults.d", "vault2")
if err := fs.MkdirAll(vaultDir2, 0700); err != nil { if err := fs.MkdirAll(vaultDir2, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err) t.Fatalf("Failed to create vault directory: %v", err)
} }
@ -112,7 +110,7 @@ func TestVaultMetadata(t *testing.T) {
pubKey5 := identity5.Recipient().String() pubKey5 := identity5.Recipient().String()
// Write public key // Write public key
if err := afero.WriteFile(fs, filepath.Join(vaultDir2, "pub.age"), []byte(pubKey5), 0600); err != nil { if err := afero.WriteFile(fs, filepath.Join(vaultDir2, "pub.age"), []byte(pubKey5), 0o600); err != nil {
t.Fatalf("Failed to write public key: %v", err) t.Fatalf("Failed to write public key: %v", err)
} }
@ -140,7 +138,7 @@ func TestVaultMetadata(t *testing.T) {
t.Run("MetadataPersistence", func(t *testing.T) { t.Run("MetadataPersistence", func(t *testing.T) {
vaultDir := filepath.Join(stateDir, "vaults.d", "test-vault") vaultDir := filepath.Join(stateDir, "vaults.d", "test-vault")
if err := fs.MkdirAll(vaultDir, 0700); err != nil { if err := fs.MkdirAll(vaultDir, 0o700); err != nil {
t.Fatalf("Failed to create vault directory: %v", err) t.Fatalf("Failed to create vault directory: %v", err)
} }

View File

@ -46,7 +46,7 @@ func createTestVaultWithKey(t *testing.T, fs afero.Fs, stateDir, vaultName strin
// Store long-term public key in vault // Store long-term public key in vault
vaultDir, _ := vault.GetDirectory() vaultDir, _ := vault.GetDirectory()
ltPubKeyPath := filepath.Join(vaultDir, "pub.age") ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600) err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Unlock the vault with the derived key // Unlock the vault with the derived key

View File

@ -190,7 +190,6 @@ func TestDeterministicXPRVDerivation(t *testing.T) {
func TestMnemonicVsXPRVConsistency(t *testing.T) { func TestMnemonicVsXPRVConsistency(t *testing.T) {
// FIXME This test is missing! // FIXME This test is missing!
} }
func TestEntropyLength(t *testing.T) { func TestEntropyLength(t *testing.T) {

View File

@ -28,9 +28,9 @@ const (
BIP85_KEY_HMAC_KEY = "bip-entropy-from-k" //nolint:revive // ALL_CAPS used for BIP85 constants BIP85_KEY_HMAC_KEY = "bip-entropy-from-k" //nolint:revive // ALL_CAPS used for BIP85 constants
// Application numbers // Application numbers
APP_BIP39 = 39 // BIP39 mnemonics //nolint:revive // ALL_CAPS used for BIP85 constants APP_BIP39 = 39 // BIP39 mnemonics //nolint:revive // ALL_CAPS used for BIP85 constants
APP_HD_WIF = 2 // WIF for Bitcoin Core //nolint:revive // ALL_CAPS used for BIP85 constants APP_HD_WIF = 2 // WIF for Bitcoin Core //nolint:revive // ALL_CAPS used for BIP85 constants
APP_XPRV = 32 // Extended private key //nolint:revive // ALL_CAPS used for BIP85 constants APP_XPRV = 32 // Extended private key //nolint:revive // ALL_CAPS used for BIP85 constants
APP_HEX = 128169 //nolint:revive // ALL_CAPS used for BIP85 constants APP_HEX = 128169 //nolint:revive // ALL_CAPS used for BIP85 constants
APP_PWD64 = 707764 // Base64 passwords //nolint:revive // ALL_CAPS used for BIP85 constants APP_PWD64 = 707764 // Base64 passwords //nolint:revive // ALL_CAPS used for BIP85 constants
APP_PWD85 = 707785 // Base85 passwords //nolint:revive // ALL_CAPS used for BIP85 constants APP_PWD85 = 707785 // Base85 passwords //nolint:revive // ALL_CAPS used for BIP85 constants

View File

@ -1,6 +1,7 @@
//nolint:gosec // G101: Test file contains BIP85 test vectors, not real credentials
package bip85 package bip85
//nolint:gosec,revive,unparam // Test file with hardcoded test vectors //nolint:revive,unparam // Test file with BIP85 test vectors
import ( import (
"bytes" "bytes"