Compare commits
	
		
			10 Commits
		
	
	
		
			533133486c
			...
			4e242c3491
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4e242c3491 | |||
| 54fce0f187 | |||
| 93a32217e0 | |||
| 95ba80f618 | |||
| d710323bd0 | |||
| 38b450cbcf | |||
| 6fe49344e2 | |||
| 6e01ae6002 | |||
| 11e43542cf | |||
| 2256a37b72 | 
@ -1,6 +1,8 @@
 | 
			
		||||
version: "2"
 | 
			
		||||
 | 
			
		||||
run:
 | 
			
		||||
  timeout: 5m
 | 
			
		||||
  go: "1.22"
 | 
			
		||||
  go: "1.24"
 | 
			
		||||
  tests: false
 | 
			
		||||
 | 
			
		||||
linters:
 | 
			
		||||
  enable:
 | 
			
		||||
@ -14,7 +16,6 @@ linters:
 | 
			
		||||
    - mnd              # An analyzer to detect magic numbers
 | 
			
		||||
    - lll              # Reports long lines
 | 
			
		||||
    - intrange         # intrange is a linter to find places where for loops could make use of an integer range
 | 
			
		||||
    - gofumpt          # Gofumpt checks whether code was gofumpt-ed
 | 
			
		||||
    - gochecknoglobals # Check that no global variables exist
 | 
			
		||||
 | 
			
		||||
    # Default/existing linters that are commonly useful
 | 
			
		||||
@ -22,11 +23,7 @@ linters:
 | 
			
		||||
    - errcheck
 | 
			
		||||
    - staticcheck
 | 
			
		||||
    - unused
 | 
			
		||||
    - gosimple
 | 
			
		||||
    - ineffassign
 | 
			
		||||
    - typecheck
 | 
			
		||||
    - gofmt
 | 
			
		||||
    - goimports
 | 
			
		||||
    - misspell
 | 
			
		||||
    - revive
 | 
			
		||||
    - gosec
 | 
			
		||||
@ -78,17 +75,14 @@ linters-settings:
 | 
			
		||||
  testifylint:
 | 
			
		||||
    enable-all: true
 | 
			
		||||
 | 
			
		||||
  usetesting:
 | 
			
		||||
    strict: true
 | 
			
		||||
  usetesting: {}
 | 
			
		||||
 | 
			
		||||
issues:
 | 
			
		||||
  max-issues-per-linter: 0
 | 
			
		||||
  max-same-issues: 0
 | 
			
		||||
  exclude-rules:
 | 
			
		||||
    # Exclude some linters from running on tests files
 | 
			
		||||
    # Exclude all linters from running on test files
 | 
			
		||||
    - path: _test\.go
 | 
			
		||||
      linters:
 | 
			
		||||
        - gochecknoglobals
 | 
			
		||||
        - mnd
 | 
			
		||||
        - unparam
 | 
			
		||||
 | 
			
		||||
    # Allow long lines in generated code or test data
 | 
			
		||||
    - path: ".*_gen\\.go"
 | 
			
		||||
@ -99,6 +93,3 @@ issues:
 | 
			
		||||
    - text: "parameter '(args|cmd)' seems to be unused"
 | 
			
		||||
      linters:
 | 
			
		||||
        - revive
 | 
			
		||||
 | 
			
		||||
  max-issues-per-linter: 0
 | 
			
		||||
  max-same-issues: 0
 | 
			
		||||
@ -47,7 +47,7 @@ func TestDetermineStateDir(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Test with custom config dir
 | 
			
		||||
	os.Unsetenv(secret.EnvStateDir)
 | 
			
		||||
	_ = os.Unsetenv(secret.EnvStateDir)
 | 
			
		||||
	customConfigDir := "/custom-config"
 | 
			
		||||
	stateDir = secret.DetermineStateDir(customConfigDir)
 | 
			
		||||
	expectedDir := filepath.Join(customConfigDir, secret.AppID)
 | 
			
		||||
 | 
			
		||||
@ -74,29 +74,7 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		return fmt.Errorf("failed to check if secret exists: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exists {
 | 
			
		||||
		// Secret exists, get the age secret key from it
 | 
			
		||||
		var secretValue []byte
 | 
			
		||||
		if os.Getenv(secret.EnvMnemonic) != "" {
 | 
			
		||||
			secretValue, err = secretObj.GetValue(nil)
 | 
			
		||||
		} else {
 | 
			
		||||
			unlocker, unlockErr := vlt.GetCurrentUnlocker()
 | 
			
		||||
			if unlockErr != nil {
 | 
			
		||||
				return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
 | 
			
		||||
			}
 | 
			
		||||
			secretValue, err = secretObj.GetValue(unlocker)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to get secret value: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ageSecretKey = string(secretValue)
 | 
			
		||||
 | 
			
		||||
		// Validate that it's a valid age secret key
 | 
			
		||||
		if !isValidAgeSecretKey(ageSecretKey) {
 | 
			
		||||
			return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
	if !exists { //nolint:nestif // Clear conditional logic for secret generation vs retrieval
 | 
			
		||||
		// Secret doesn't exist, generate new age key and store it
 | 
			
		||||
		identity, err := age.GenerateX25519Identity()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -110,6 +88,19 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to store age key: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// Secret exists, get the age secret key from it
 | 
			
		||||
		secretValue, err := cli.getSecretValue(vlt, secretObj)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to get secret value: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ageSecretKey = string(secretValue)
 | 
			
		||||
 | 
			
		||||
		// Validate that it's a valid age secret key
 | 
			
		||||
		if !isValidAgeSecretKey(ageSecretKey) {
 | 
			
		||||
			return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse the secret key
 | 
			
		||||
@ -128,7 +119,7 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to open input file: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
		defer func() { _ = file.Close() }()
 | 
			
		||||
		input = file
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -139,7 +130,7 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to create output file: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
		defer func() { _ = file.Close() }()
 | 
			
		||||
		output = file
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -214,7 +205,7 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to open input file: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
		defer func() { _ = file.Close() }()
 | 
			
		||||
		input = file
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -225,7 +216,7 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to create output file: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
		defer func() { _ = file.Close() }()
 | 
			
		||||
		output = file
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -247,3 +238,17 @@ func isValidAgeSecretKey(key string) bool {
 | 
			
		||||
	_, err := age.ParseX25519Identity(key)
 | 
			
		||||
	return err == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getSecretValue retrieves the value of a secret using the appropriate unlocker
 | 
			
		||||
func (cli *Instance) getSecretValue(vlt *vault.Vault, secretObj *secret.Secret) ([]byte, error) {
 | 
			
		||||
	if os.Getenv(secret.EnvMnemonic) != "" {
 | 
			
		||||
		return secretObj.GetValue(nil)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	unlocker, err := vlt.GetCurrentUnlocker()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to get current unlocker: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	return secretObj.GetValue(unlocker)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,11 @@ import (
 | 
			
		||||
	"github.com/tyler-smith/go-bip39"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	defaultSecretLength = 16
 | 
			
		||||
	mnemonicEntropyBits = 128
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newGenerateCmd() *cobra.Command {
 | 
			
		||||
	cmd := &cobra.Command{
 | 
			
		||||
		Use:   "generate",
 | 
			
		||||
@ -55,7 +60,7 @@ func newGenerateSecretCmd() *cobra.Command {
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmd.Flags().IntP("length", "l", 16, "Length of the generated secret (default 16)")
 | 
			
		||||
	cmd.Flags().IntP("length", "l", defaultSecretLength, "Length of the generated secret (default 16)")
 | 
			
		||||
	cmd.Flags().StringP("type", "t", "base58", "Type of secret to generate (base58, alnum)")
 | 
			
		||||
	cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret")
 | 
			
		||||
 | 
			
		||||
@ -65,7 +70,7 @@ func newGenerateSecretCmd() *cobra.Command {
 | 
			
		||||
// GenerateMnemonic generates a random BIP39 mnemonic phrase
 | 
			
		||||
func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error {
 | 
			
		||||
	// Generate 128 bits of entropy for a 12-word mnemonic
 | 
			
		||||
	entropy, err := bip39.NewEntropy(128)
 | 
			
		||||
	entropy, err := bip39.NewEntropy(mnemonicEntropyBits)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to generate entropy: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -80,12 +80,12 @@ func (cli *Instance) Init(cmd *cobra.Command) error {
 | 
			
		||||
 | 
			
		||||
	// Set mnemonic in environment for CreateVault to use
 | 
			
		||||
	originalMnemonic := os.Getenv(secret.EnvMnemonic)
 | 
			
		||||
	os.Setenv(secret.EnvMnemonic, mnemonicStr)
 | 
			
		||||
	_ = os.Setenv(secret.EnvMnemonic, mnemonicStr)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if originalMnemonic != "" {
 | 
			
		||||
			os.Setenv(secret.EnvMnemonic, originalMnemonic)
 | 
			
		||||
			_ = os.Setenv(secret.EnvMnemonic, originalMnemonic)
 | 
			
		||||
		} else {
 | 
			
		||||
			os.Unsetenv(secret.EnvMnemonic)
 | 
			
		||||
			_ = os.Unsetenv(secret.EnvMnemonic)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -214,7 +214,7 @@ func (cli *Instance) ListSecrets(cmd *cobra.Command, jsonOutput bool, filter str
 | 
			
		||||
		filteredSecrets = secrets
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if jsonOutput {
 | 
			
		||||
	if jsonOutput { //nolint:nestif // Separate JSON and table output formatting logic
 | 
			
		||||
		// For JSON output, get metadata for each secret
 | 
			
		||||
		secretsWithMetadata := make([]map[string]interface{}, 0, len(filteredSecrets))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ func ExecuteCommandInProcess(args []string, stdin string, env map[string]string)
 | 
			
		||||
 | 
			
		||||
	// Set test environment
 | 
			
		||||
	for k, v := range env {
 | 
			
		||||
		os.Setenv(k, v)
 | 
			
		||||
		_ = os.Setenv(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create root command
 | 
			
		||||
@ -53,9 +53,9 @@ func ExecuteCommandInProcess(args []string, stdin string, env map[string]string)
 | 
			
		||||
	// Restore environment
 | 
			
		||||
	for k, v := range savedEnv {
 | 
			
		||||
		if v == "" {
 | 
			
		||||
			os.Unsetenv(k)
 | 
			
		||||
			_ = os.Unsetenv(k)
 | 
			
		||||
		} else {
 | 
			
		||||
			os.Setenv(k, v)
 | 
			
		||||
			_ = os.Setenv(k, v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -98,7 +98,7 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if jsonOutput {
 | 
			
		||||
	if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
 | 
			
		||||
		// Get current vault name for context
 | 
			
		||||
		currentVault := ""
 | 
			
		||||
		if currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir); err == nil {
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,10 @@ import (
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	tabWriterPadding = 2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// newVersionCmd returns the version management command
 | 
			
		||||
func newVersionCmd() *cobra.Command {
 | 
			
		||||
	cli := NewCLIInstance()
 | 
			
		||||
@ -41,7 +45,7 @@ func VersionCommands(cli *Instance) *cobra.Command {
 | 
			
		||||
		Use:   "promote <secret-name> <version>",
 | 
			
		||||
		Short: "Promote a specific version to current",
 | 
			
		||||
		Long:  "Updates the current symlink to point to the specified version without modifying timestamps",
 | 
			
		||||
		Args:  cobra.ExactArgs(2),
 | 
			
		||||
		Args:  cobra.ExactArgs(2), //nolint:mnd // Command requires exactly 2 arguments: secret-name and version
 | 
			
		||||
		RunE: func(cmd *cobra.Command, args []string) error {
 | 
			
		||||
			return cli.PromoteVersion(cmd, args[0], args[1])
 | 
			
		||||
		},
 | 
			
		||||
@ -110,8 +114,8 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create table writer
 | 
			
		||||
	w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
 | 
			
		||||
	fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER")
 | 
			
		||||
	w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, tabWriterPadding, ' ', 0)
 | 
			
		||||
	_, _ = fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER")
 | 
			
		||||
 | 
			
		||||
	// Load and display each version's metadata
 | 
			
		||||
	for _, version := range versions {
 | 
			
		||||
@ -125,7 +129,7 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
 | 
			
		||||
			if version == currentVersion {
 | 
			
		||||
				status = "current (error)"
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, "-", status, "-", "-")
 | 
			
		||||
			_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, "-", status, "-", "-")
 | 
			
		||||
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
@ -152,10 +156,10 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
 | 
			
		||||
			notAfter = sv.Metadata.NotAfter.Format("2006-01-02 15:04:05")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, createdAt, status, notBefore, notAfter)
 | 
			
		||||
		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, createdAt, status, notBefore, notAfter)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Flush()
 | 
			
		||||
	_ = w.Flush()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	debugEnabled bool
 | 
			
		||||
	debugLogger  *slog.Logger
 | 
			
		||||
	debugEnabled bool         //nolint:gochecknoglobals // Package-wide debug state is necessary
 | 
			
		||||
	debugLogger  *slog.Logger //nolint:gochecknoglobals // Package-wide logger instance is necessary
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,10 @@ import (
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	agePrivKeyPassphraseLength = 64
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// keychainItemNameRegex validates keychain item names
 | 
			
		||||
// Allows alphanumeric characters, dots, hyphens, and underscores only
 | 
			
		||||
var keychainItemNameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
 | 
			
		||||
@ -253,7 +257,7 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Step 2: Generate a random passphrase for encrypting the age private key
 | 
			
		||||
	agePrivKeyPassphrase, err := generateRandomPassphrase(64)
 | 
			
		||||
	agePrivKeyPassphrase, err := generateRandomPassphrase(agePrivKeyPassphraseLength)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -20,11 +20,11 @@ import (
 | 
			
		||||
var (
 | 
			
		||||
	// GPGEncryptFunc is the function used for GPG encryption
 | 
			
		||||
	// Can be overridden in tests to provide a non-interactive implementation
 | 
			
		||||
	GPGEncryptFunc = gpgEncryptDefault
 | 
			
		||||
	GPGEncryptFunc = gpgEncryptDefault //nolint:gochecknoglobals // Required for test mocking
 | 
			
		||||
 | 
			
		||||
	// GPGDecryptFunc is the function used for GPG decryption
 | 
			
		||||
	// Can be overridden in tests to provide a non-interactive implementation
 | 
			
		||||
	GPGDecryptFunc = gpgDecryptDefault
 | 
			
		||||
	GPGDecryptFunc = gpgDecryptDefault //nolint:gochecknoglobals // Required for test mocking
 | 
			
		||||
 | 
			
		||||
	// gpgKeyIDRegex validates GPG key IDs
 | 
			
		||||
	// Allows either:
 | 
			
		||||
 | 
			
		||||
@ -286,6 +286,8 @@ func GetCurrentVault(fs afero.Fs, stateDir string) (VaultInterface, error) {
 | 
			
		||||
 | 
			
		||||
// getCurrentVaultFunc is a function variable that will be set by the vault package
 | 
			
		||||
// to implement the actual GetCurrentVault functionality
 | 
			
		||||
//
 | 
			
		||||
//nolint:gochecknoglobals // Required to break import cycle
 | 
			
		||||
var getCurrentVaultFunc func(fs afero.Fs, stateDir string) (VaultInterface, error)
 | 
			
		||||
 | 
			
		||||
// RegisterGetCurrentVaultFunc allows the vault package to register its implementation
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,11 @@ import (
 | 
			
		||||
	"github.com/spf13/afero"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	versionNameParts  = 2
 | 
			
		||||
	maxVersionsPerDay = 999
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// VersionMetadata contains information about a secret version
 | 
			
		||||
type VersionMetadata struct {
 | 
			
		||||
	ID        string     `json:"id"`                  // ULID
 | 
			
		||||
@ -87,7 +92,7 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
 | 
			
		||||
		if entry.IsDir() && strings.HasPrefix(entry.Name(), prefix) {
 | 
			
		||||
			// Extract serial number
 | 
			
		||||
			parts := strings.Split(entry.Name(), ".")
 | 
			
		||||
			if len(parts) == 2 {
 | 
			
		||||
			if len(parts) == versionNameParts {
 | 
			
		||||
				var serial int
 | 
			
		||||
				if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err == nil {
 | 
			
		||||
					if serial > maxSerial {
 | 
			
		||||
@ -100,7 +105,7 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
 | 
			
		||||
 | 
			
		||||
	// Generate new version name
 | 
			
		||||
	newSerial := maxSerial + 1
 | 
			
		||||
	if newSerial > 999 {
 | 
			
		||||
	if newSerial > maxVersionsPerDay {
 | 
			
		||||
		return "", fmt.Errorf("exceeded maximum versions per day (999)")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,10 +21,11 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	purpose  = uint32(83696968)  // fixed by BIP-85 ("bip")
 | 
			
		||||
	vendorID = uint32(592366788) // berlin.sneak
 | 
			
		||||
	appID    = uint32(733482323) // secret
 | 
			
		||||
	hrp      = "age-secret-key-" // Bech32 HRP used by age
 | 
			
		||||
	purpose      = uint32(83696968)  // fixed by BIP-85 ("bip")
 | 
			
		||||
	vendorID     = uint32(592366788) // berlin.sneak
 | 
			
		||||
	appID        = uint32(733482323) // secret
 | 
			
		||||
	hrp          = "age-secret-key-" // Bech32 HRP used by age
 | 
			
		||||
	x25519KeySize = 32               // 256-bit key size for X25519
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// clamp applies RFC-7748 clamping to a 32-byte scalar.
 | 
			
		||||
@ -37,16 +38,20 @@ func clamp(k []byte) {
 | 
			
		||||
// IdentityFromEntropy converts 32 deterministic bytes into an
 | 
			
		||||
// *age.X25519Identity by round-tripping through Bech32.
 | 
			
		||||
func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
 | 
			
		||||
	if len(ent) != 32 { // 32 bytes = 256-bit key size for X25519
 | 
			
		||||
	if len(ent) != x25519KeySize {
 | 
			
		||||
		return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make a copy to avoid modifying the original
 | 
			
		||||
	key := make([]byte, 32) // 32 bytes = 256-bit key size for X25519 // 32 bytes = 256-bit key size for X25519
 | 
			
		||||
	key := make([]byte, x25519KeySize)
 | 
			
		||||
	copy(key, ent)
 | 
			
		||||
	clamp(key)
 | 
			
		||||
 | 
			
		||||
	data, err := bech32.ConvertBits(key, 8, 5, true) // Convert from 8-bit to 5-bit encoding for bech32
 | 
			
		||||
	const (
 | 
			
		||||
		bech32BitSize8 = 8 // Standard 8-bit encoding
 | 
			
		||||
		bech32BitSize5 = 5 // Bech32 5-bit encoding
 | 
			
		||||
	)
 | 
			
		||||
	data, err := bech32.ConvertBits(key, bech32BitSize8, bech32BitSize5, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("bech32 convert: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
@ -81,7 +86,7 @@ func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) {
 | 
			
		||||
 | 
			
		||||
	// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
 | 
			
		||||
	drng := bip85.NewBIP85DRNG(entropy)
 | 
			
		||||
	key := make([]byte, 32) // 32 bytes = 256-bit key size for X25519
 | 
			
		||||
	key := make([]byte, x25519KeySize)
 | 
			
		||||
	_, err = drng.Read(key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to read from DRNG: %w", err)
 | 
			
		||||
@ -110,7 +115,7 @@ func DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error) {
 | 
			
		||||
 | 
			
		||||
	// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
 | 
			
		||||
	drng := bip85.NewBIP85DRNG(entropy)
 | 
			
		||||
	key := make([]byte, 32) // 32 bytes = 256-bit key size for X25519
 | 
			
		||||
	key := make([]byte, x25519KeySize)
 | 
			
		||||
	_, err = drng.Read(key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to read from DRNG: %w", err)
 | 
			
		||||
 | 
			
		||||
@ -40,9 +40,9 @@ const (
 | 
			
		||||
// Version bytes for extended keys
 | 
			
		||||
var (
 | 
			
		||||
	// MainNetPrivateKey is the version for mainnet private keys
 | 
			
		||||
	MainNetPrivateKey = []byte{0x04, 0x88, 0xAD, 0xE4}
 | 
			
		||||
	MainNetPrivateKey = []byte{0x04, 0x88, 0xAD, 0xE4} //nolint:gochecknoglobals // Standard BIP32 constant
 | 
			
		||||
	// TestNetPrivateKey is the version for testnet private keys
 | 
			
		||||
	TestNetPrivateKey = []byte{0x04, 0x35, 0x83, 0x94}
 | 
			
		||||
	TestNetPrivateKey = []byte{0x04, 0x35, 0x83, 0x94} //nolint:gochecknoglobals // Standard BIP32 constant
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DRNG is a deterministic random number generator seeded by BIP85 entropy
 | 
			
		||||
@ -52,14 +52,15 @@ type DRNG struct {
 | 
			
		||||
 | 
			
		||||
// NewBIP85DRNG creates a new DRNG seeded with BIP85 entropy
 | 
			
		||||
func NewBIP85DRNG(entropy []byte) *DRNG {
 | 
			
		||||
	const bip85EntropySize = 64 // 512 bits
 | 
			
		||||
	// The entropy must be exactly 64 bytes (512 bits)
 | 
			
		||||
	if len(entropy) != 64 {
 | 
			
		||||
	if len(entropy) != bip85EntropySize {
 | 
			
		||||
		panic("DRNG entropy must be 64 bytes")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Initialize SHAKE256 with the entropy
 | 
			
		||||
	shake := sha3.NewShake256()
 | 
			
		||||
	shake.Write(entropy)
 | 
			
		||||
	_, _ = shake.Write(entropy) // Write to hash functions never returns an error
 | 
			
		||||
 | 
			
		||||
	return &DRNG{
 | 
			
		||||
		shake: shake,
 | 
			
		||||
@ -169,17 +170,26 @@ func DeriveBIP39Entropy(masterKey *hdkeychain.ExtendedKey, language, words, inde
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Determine how many bits of entropy to use based on the words
 | 
			
		||||
	// BIP39 defines specific word counts and their corresponding entropy bits
 | 
			
		||||
	const (
 | 
			
		||||
		words12 = 12 // 128 bits of entropy
 | 
			
		||||
		words15 = 15 // 160 bits of entropy
 | 
			
		||||
		words18 = 18 // 192 bits of entropy
 | 
			
		||||
		words21 = 21 // 224 bits of entropy
 | 
			
		||||
		words24 = 24 // 256 bits of entropy
 | 
			
		||||
	)
 | 
			
		||||
	
 | 
			
		||||
	var bits int
 | 
			
		||||
	switch words {
 | 
			
		||||
	case 12:
 | 
			
		||||
	case words12:
 | 
			
		||||
		bits = 128
 | 
			
		||||
	case 15:
 | 
			
		||||
	case words15:
 | 
			
		||||
		bits = 160
 | 
			
		||||
	case 18:
 | 
			
		||||
	case words18:
 | 
			
		||||
		bits = 192
 | 
			
		||||
	case 21:
 | 
			
		||||
	case words21:
 | 
			
		||||
		bits = 224
 | 
			
		||||
	case 24:
 | 
			
		||||
	case words24:
 | 
			
		||||
		bits = 256
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("invalid BIP39 word count: %d", words)
 | 
			
		||||
@ -345,24 +355,30 @@ func encodeBase85WithRFC1924Charset(data []byte) string {
 | 
			
		||||
	// RFC1924 character set
 | 
			
		||||
	charset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"
 | 
			
		||||
 | 
			
		||||
	const (
 | 
			
		||||
		base85ChunkSize  = 4  // Process 4 bytes at a time
 | 
			
		||||
		base85DigitCount = 5  // Each chunk produces 5 digits
 | 
			
		||||
		base85Base       = 85 // Base85 encoding uses base 85
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Pad data to multiple of 4
 | 
			
		||||
	padded := make([]byte, ((len(data)+3)/4)*4)
 | 
			
		||||
	padded := make([]byte, ((len(data)+base85ChunkSize-1)/base85ChunkSize)*base85ChunkSize)
 | 
			
		||||
	copy(padded, data)
 | 
			
		||||
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	buf.Grow(len(padded) * 5 / 4) // Each 4 bytes becomes 5 Base85 characters
 | 
			
		||||
	buf.Grow(len(padded) * base85DigitCount / base85ChunkSize) // Each 4 bytes becomes 5 Base85 characters
 | 
			
		||||
 | 
			
		||||
	// Process in 4-byte chunks
 | 
			
		||||
	for i := 0; i < len(padded); i += 4 {
 | 
			
		||||
	for i := 0; i < len(padded); i += base85ChunkSize {
 | 
			
		||||
		// Convert 4 bytes to uint32 (big-endian)
 | 
			
		||||
		chunk := binary.BigEndian.Uint32(padded[i : i+4])
 | 
			
		||||
		chunk := binary.BigEndian.Uint32(padded[i : i+base85ChunkSize])
 | 
			
		||||
 | 
			
		||||
		// Convert to 5 base-85 digits
 | 
			
		||||
		digits := make([]byte, 5)
 | 
			
		||||
		for j := 4; j >= 0; j-- {
 | 
			
		||||
			idx := chunk % 85
 | 
			
		||||
		digits := make([]byte, base85DigitCount)
 | 
			
		||||
		for j := base85DigitCount - 1; j >= 0; j-- {
 | 
			
		||||
			idx := chunk % base85Base
 | 
			
		||||
			digits[j] = charset[idx]
 | 
			
		||||
			chunk /= 85
 | 
			
		||||
			chunk /= base85Base
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buf.Write(digits)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user