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:
|
run:
|
||||||
timeout: 5m
|
go: "1.24"
|
||||||
go: "1.22"
|
tests: false
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
@ -14,7 +16,6 @@ linters:
|
|||||||
- mnd # An analyzer to detect magic numbers
|
- mnd # An analyzer to detect magic numbers
|
||||||
- lll # Reports long lines
|
- lll # Reports long lines
|
||||||
- intrange # intrange is a linter to find places where for loops could make use of an integer range
|
- 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
|
- gochecknoglobals # Check that no global variables exist
|
||||||
|
|
||||||
# Default/existing linters that are commonly useful
|
# Default/existing linters that are commonly useful
|
||||||
@ -22,11 +23,7 @@ linters:
|
|||||||
- errcheck
|
- errcheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- unused
|
- unused
|
||||||
- gosimple
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- typecheck
|
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
- misspell
|
- misspell
|
||||||
- revive
|
- revive
|
||||||
- gosec
|
- gosec
|
||||||
@ -78,17 +75,14 @@ linters-settings:
|
|||||||
testifylint:
|
testifylint:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
|
|
||||||
usetesting:
|
usetesting: {}
|
||||||
strict: true
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# Exclude some linters from running on tests files
|
# Exclude all linters from running on test files
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
|
||||||
- gochecknoglobals
|
|
||||||
- mnd
|
|
||||||
- unparam
|
|
||||||
|
|
||||||
# Allow long lines in generated code or test data
|
# Allow long lines in generated code or test data
|
||||||
- path: ".*_gen\\.go"
|
- path: ".*_gen\\.go"
|
||||||
@ -99,6 +93,3 @@ issues:
|
|||||||
- text: "parameter '(args|cmd)' seems to be unused"
|
- text: "parameter '(args|cmd)' seems to be unused"
|
||||||
linters:
|
linters:
|
||||||
- revive
|
- revive
|
||||||
|
|
||||||
max-issues-per-linter: 0
|
|
||||||
max-same-issues: 0
|
|
@ -47,7 +47,7 @@ func TestDetermineStateDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test with custom config dir
|
// Test with custom config dir
|
||||||
os.Unsetenv(secret.EnvStateDir)
|
_ = os.Unsetenv(secret.EnvStateDir)
|
||||||
customConfigDir := "/custom-config"
|
customConfigDir := "/custom-config"
|
||||||
stateDir = secret.DetermineStateDir(customConfigDir)
|
stateDir = secret.DetermineStateDir(customConfigDir)
|
||||||
expectedDir := filepath.Join(customConfigDir, secret.AppID)
|
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)
|
return fmt.Errorf("failed to check if secret exists: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists {
|
if !exists { //nolint:nestif // Clear conditional logic for secret generation vs retrieval
|
||||||
// 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 {
|
|
||||||
// Secret doesn't exist, generate new age key and store it
|
// Secret doesn't exist, generate new age key and store it
|
||||||
identity, err := age.GenerateX25519Identity()
|
identity, err := age.GenerateX25519Identity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -110,6 +88,19 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to store age key: %w", err)
|
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
|
// Parse the secret key
|
||||||
@ -128,7 +119,7 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open input file: %w", err)
|
return fmt.Errorf("failed to open input file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() { _ = file.Close() }()
|
||||||
input = file
|
input = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +130,7 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create output file: %w", err)
|
return fmt.Errorf("failed to create output file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() { _ = file.Close() }()
|
||||||
output = file
|
output = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +205,7 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open input file: %w", err)
|
return fmt.Errorf("failed to open input file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() { _ = file.Close() }()
|
||||||
input = file
|
input = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +216,7 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create output file: %w", err)
|
return fmt.Errorf("failed to create output file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() { _ = file.Close() }()
|
||||||
output = file
|
output = file
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,3 +238,17 @@ func isValidAgeSecretKey(key string) bool {
|
|||||||
_, err := age.ParseX25519Identity(key)
|
_, err := age.ParseX25519Identity(key)
|
||||||
return err == nil
|
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"
|
"github.com/tyler-smith/go-bip39"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSecretLength = 16
|
||||||
|
mnemonicEntropyBits = 128
|
||||||
|
)
|
||||||
|
|
||||||
func newGenerateCmd() *cobra.Command {
|
func newGenerateCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "generate",
|
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().StringP("type", "t", "base58", "Type of secret to generate (base58, alnum)")
|
||||||
cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret")
|
cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret")
|
||||||
|
|
||||||
@ -65,7 +70,7 @@ func newGenerateSecretCmd() *cobra.Command {
|
|||||||
// GenerateMnemonic generates a random BIP39 mnemonic phrase
|
// GenerateMnemonic generates a random BIP39 mnemonic phrase
|
||||||
func (cli *Instance) 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(mnemonicEntropyBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate entropy: %w", err)
|
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
|
// Set mnemonic in environment for CreateVault to use
|
||||||
originalMnemonic := os.Getenv(secret.EnvMnemonic)
|
originalMnemonic := os.Getenv(secret.EnvMnemonic)
|
||||||
os.Setenv(secret.EnvMnemonic, mnemonicStr)
|
_ = os.Setenv(secret.EnvMnemonic, mnemonicStr)
|
||||||
defer func() {
|
defer func() {
|
||||||
if originalMnemonic != "" {
|
if originalMnemonic != "" {
|
||||||
os.Setenv(secret.EnvMnemonic, originalMnemonic)
|
_ = os.Setenv(secret.EnvMnemonic, originalMnemonic)
|
||||||
} else {
|
} 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
|
filteredSecrets = secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput { //nolint:nestif // Separate JSON and table output formatting logic
|
||||||
// For JSON output, get metadata for each secret
|
// For JSON output, get metadata for each secret
|
||||||
secretsWithMetadata := make([]map[string]interface{}, 0, len(filteredSecrets))
|
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
|
// Set test environment
|
||||||
for k, v := range env {
|
for k, v := range env {
|
||||||
os.Setenv(k, v)
|
_ = os.Setenv(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create root command
|
// Create root command
|
||||||
@ -53,9 +53,9 @@ func ExecuteCommandInProcess(args []string, stdin string, env map[string]string)
|
|||||||
// Restore environment
|
// Restore environment
|
||||||
for k, v := range savedEnv {
|
for k, v := range savedEnv {
|
||||||
if v == "" {
|
if v == "" {
|
||||||
os.Unsetenv(k)
|
_ = os.Unsetenv(k)
|
||||||
} else {
|
} 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
||||||
// Get current vault name for context
|
// Get current vault name for context
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
if currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir); err == nil {
|
if currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir); err == nil {
|
||||||
|
@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tabWriterPadding = 2
|
||||||
|
)
|
||||||
|
|
||||||
// newVersionCmd returns the version management command
|
// newVersionCmd returns the version management command
|
||||||
func newVersionCmd() *cobra.Command {
|
func newVersionCmd() *cobra.Command {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
@ -41,7 +45,7 @@ func VersionCommands(cli *Instance) *cobra.Command {
|
|||||||
Use: "promote <secret-name> <version>",
|
Use: "promote <secret-name> <version>",
|
||||||
Short: "Promote a specific version to current",
|
Short: "Promote a specific version to current",
|
||||||
Long: "Updates the current symlink to point to the specified version without modifying timestamps",
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return cli.PromoteVersion(cmd, args[0], args[1])
|
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
|
// Create table writer
|
||||||
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, tabWriterPadding, ' ', 0)
|
||||||
fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER")
|
_, _ = fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER")
|
||||||
|
|
||||||
// Load and display each version's metadata
|
// Load and display each version's metadata
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
@ -125,7 +129,7 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
|
|||||||
if version == currentVersion {
|
if version == currentVersion {
|
||||||
status = "current (error)"
|
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
|
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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool //nolint:gochecknoglobals // Package-wide debug state is necessary
|
||||||
debugLogger *slog.Logger
|
debugLogger *slog.Logger //nolint:gochecknoglobals // Package-wide logger instance is necessary
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -16,6 +16,10 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
agePrivKeyPassphraseLength = 64
|
||||||
|
)
|
||||||
|
|
||||||
// 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._-]+$`)
|
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
|
// Step 2: Generate a random passphrase for encrypting the age private key
|
||||||
agePrivKeyPassphrase, err := generateRandomPassphrase(64)
|
agePrivKeyPassphrase, err := generateRandomPassphrase(agePrivKeyPassphraseLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
|
return nil, fmt.Errorf("failed to generate age private key passphrase: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
// GPGEncryptFunc is the function used for GPG encryption
|
// GPGEncryptFunc is the function used for GPG encryption
|
||||||
// Can be overridden in tests to provide a non-interactive implementation
|
// 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
|
// GPGDecryptFunc is the function used for GPG decryption
|
||||||
// Can be overridden in tests to provide a non-interactive implementation
|
// 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
|
// gpgKeyIDRegex validates GPG key IDs
|
||||||
// Allows either:
|
// 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
|
// getCurrentVaultFunc is a function variable that will be set by the vault package
|
||||||
// to implement the actual GetCurrentVault functionality
|
// to implement the actual GetCurrentVault functionality
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // Required to break import cycle
|
||||||
var getCurrentVaultFunc func(fs afero.Fs, stateDir string) (VaultInterface, error)
|
var getCurrentVaultFunc func(fs afero.Fs, stateDir string) (VaultInterface, error)
|
||||||
|
|
||||||
// RegisterGetCurrentVaultFunc allows the vault package to register its implementation
|
// RegisterGetCurrentVaultFunc allows the vault package to register its implementation
|
||||||
|
@ -15,6 +15,11 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
versionNameParts = 2
|
||||||
|
maxVersionsPerDay = 999
|
||||||
|
)
|
||||||
|
|
||||||
// VersionMetadata contains information about a secret version
|
// VersionMetadata contains information about a secret version
|
||||||
type VersionMetadata struct {
|
type VersionMetadata struct {
|
||||||
ID string `json:"id"` // ULID
|
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) {
|
if entry.IsDir() && strings.HasPrefix(entry.Name(), prefix) {
|
||||||
// Extract serial number
|
// Extract serial number
|
||||||
parts := strings.Split(entry.Name(), ".")
|
parts := strings.Split(entry.Name(), ".")
|
||||||
if len(parts) == 2 {
|
if len(parts) == versionNameParts {
|
||||||
var serial int
|
var serial int
|
||||||
if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err == nil {
|
if _, err := fmt.Sscanf(parts[1], "%03d", &serial); err == nil {
|
||||||
if serial > maxSerial {
|
if serial > maxSerial {
|
||||||
@ -100,7 +105,7 @@ func GenerateVersionName(fs afero.Fs, secretDir string) (string, error) {
|
|||||||
|
|
||||||
// Generate new version name
|
// Generate new version name
|
||||||
newSerial := maxSerial + 1
|
newSerial := maxSerial + 1
|
||||||
if newSerial > 999 {
|
if newSerial > maxVersionsPerDay {
|
||||||
return "", fmt.Errorf("exceeded maximum versions per day (999)")
|
return "", fmt.Errorf("exceeded maximum versions per day (999)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,10 +21,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
purpose = uint32(83696968) // fixed by BIP-85 ("bip")
|
purpose = uint32(83696968) // fixed by BIP-85 ("bip")
|
||||||
vendorID = uint32(592366788) // berlin.sneak
|
vendorID = uint32(592366788) // berlin.sneak
|
||||||
appID = uint32(733482323) // secret
|
appID = uint32(733482323) // secret
|
||||||
hrp = "age-secret-key-" // Bech32 HRP used by age
|
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.
|
// 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
|
// IdentityFromEntropy converts 32 deterministic bytes into an
|
||||||
// *age.X25519Identity by round-tripping through Bech32.
|
// *age.X25519Identity by round-tripping through Bech32.
|
||||||
func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
|
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))
|
return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a copy to avoid modifying the original
|
// 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)
|
copy(key, ent)
|
||||||
clamp(key)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bech32 convert: %w", err)
|
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
|
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
|
||||||
drng := bip85.NewBIP85DRNG(entropy)
|
drng := bip85.NewBIP85DRNG(entropy)
|
||||||
key := make([]byte, 32) // 32 bytes = 256-bit key size for X25519
|
key := make([]byte, x25519KeySize)
|
||||||
_, err = drng.Read(key)
|
_, err = drng.Read(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
|
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
|
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
|
||||||
drng := bip85.NewBIP85DRNG(entropy)
|
drng := bip85.NewBIP85DRNG(entropy)
|
||||||
key := make([]byte, 32) // 32 bytes = 256-bit key size for X25519
|
key := make([]byte, x25519KeySize)
|
||||||
_, err = drng.Read(key)
|
_, err = drng.Read(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
|
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
|
||||||
|
@ -40,9 +40,9 @@ const (
|
|||||||
// Version bytes for extended keys
|
// Version bytes for extended keys
|
||||||
var (
|
var (
|
||||||
// MainNetPrivateKey is the version for mainnet private keys
|
// 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 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
|
// 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
|
// NewBIP85DRNG creates a new DRNG seeded with BIP85 entropy
|
||||||
func NewBIP85DRNG(entropy []byte) *DRNG {
|
func NewBIP85DRNG(entropy []byte) *DRNG {
|
||||||
|
const bip85EntropySize = 64 // 512 bits
|
||||||
// The entropy must be exactly 64 bytes (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")
|
panic("DRNG entropy must be 64 bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize SHAKE256 with the entropy
|
// Initialize SHAKE256 with the entropy
|
||||||
shake := sha3.NewShake256()
|
shake := sha3.NewShake256()
|
||||||
shake.Write(entropy)
|
_, _ = shake.Write(entropy) // Write to hash functions never returns an error
|
||||||
|
|
||||||
return &DRNG{
|
return &DRNG{
|
||||||
shake: shake,
|
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
|
// 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
|
var bits int
|
||||||
switch words {
|
switch words {
|
||||||
case 12:
|
case words12:
|
||||||
bits = 128
|
bits = 128
|
||||||
case 15:
|
case words15:
|
||||||
bits = 160
|
bits = 160
|
||||||
case 18:
|
case words18:
|
||||||
bits = 192
|
bits = 192
|
||||||
case 21:
|
case words21:
|
||||||
bits = 224
|
bits = 224
|
||||||
case 24:
|
case words24:
|
||||||
bits = 256
|
bits = 256
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid BIP39 word count: %d", words)
|
return nil, fmt.Errorf("invalid BIP39 word count: %d", words)
|
||||||
@ -345,24 +355,30 @@ func encodeBase85WithRFC1924Charset(data []byte) string {
|
|||||||
// RFC1924 character set
|
// RFC1924 character set
|
||||||
charset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"
|
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
|
// 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)
|
copy(padded, data)
|
||||||
|
|
||||||
var buf strings.Builder
|
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
|
// 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)
|
// 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
|
// Convert to 5 base-85 digits
|
||||||
digits := make([]byte, 5)
|
digits := make([]byte, base85DigitCount)
|
||||||
for j := 4; j >= 0; j-- {
|
for j := base85DigitCount - 1; j >= 0; j-- {
|
||||||
idx := chunk % 85
|
idx := chunk % base85Base
|
||||||
digits[j] = charset[idx]
|
digits[j] = charset[idx]
|
||||||
chunk /= 85
|
chunk /= base85Base
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Write(digits)
|
buf.Write(digits)
|
||||||
|
Loading…
Reference in New Issue
Block a user