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"
|
||||
@ -98,7 +92,4 @@ issues:
|
||||
# Exclude unused parameter warnings for cobra command signatures
|
||||
- text: "parameter '(args|cmd)' seems to be unused"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
- revive
|
@ -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