Files
secret/pkg/agehd/agehd.go
sneak 9ee216f629
All checks were successful
check / check (push) Successful in 1m8s
Update golangci-lint to v2.12.2 with canonical config
- Replace .golangci.yml with the canonical strict config (all linters
  enabled except the standard disable list; lll 88, funlen 80/50,
  cyclop 15, dupl 100; test files now linted)
- Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by
  tag and digest (Debian-based)
- Fix all ~1550 findings surfaced by the new config: line wrapping,
  wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel
  errors, perfsprint/modernize rewrites, goconst constants, thelper,
  testifylint, noctx CommandContext, testpackage conversions,
  t.Parallel() where safe, and complexity/dupl helper extraction
- Record the change and follow-up items in TODO.md
2026-08-07 17:27:23 +00:00

157 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package agehd derives deterministic X25519 age identities from a
// BIP-39 seed using a vendor/application-scoped BIP-85 path:
//
// m / 83696968 / <vendor id> / <application id> / n
//
// • vendor id = 592 366 788 (sha256("berlin.sneak") & 0x7fffffff)
// • app id = 733 482 323 (sha256("secret") & 0x7fffffff)
// • n = sequential index (0,1,…)
package agehd
import (
"errors"
"fmt"
"strings"
"filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/bip85"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/bech32"
"github.com/tyler-smith/go-bip39"
)
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
x25519KeySize = 32 // 256-bit key size for X25519
)
// errInvalidScalarSize is returned when the entropy is not exactly 32
// bytes long.
var errInvalidScalarSize = errors.New("need 32-byte scalar")
// clamp applies RFC-7748 clamping to a 32-byte scalar.
func clamp(k []byte) {
k[0] &= 248
k[31] &= 127
k[31] |= 64
}
// IdentityFromEntropy converts 32 deterministic bytes into an
// *age.X25519Identity by round-tripping through Bech32.
func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
if len(ent) != x25519KeySize {
return nil, fmt.Errorf("%w, got %d", errInvalidScalarSize, len(ent))
}
// Make a copy to avoid modifying the original
key := make([]byte, x25519KeySize)
copy(key, ent)
clamp(key)
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)
}
s, err := bech32.Encode(hrp, data)
if err != nil {
return nil, fmt.Errorf("bech32 encode: %w", err)
}
return age.ParseX25519Identity(strings.ToUpper(s))
}
// DeriveEntropy derives 32 bytes of application-scoped entropy from the
// supplied BIP-39 mnemonic and index n using BIP85.
func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) {
// Convert mnemonic to seed
seed := bip39.NewSeed(mnemonic, "")
// Create master key from seed
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
return nil, fmt.Errorf("failed to create master key: %w", err)
}
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
// Derive BIP85 entropy (64 bytes)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
}
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
drng := bip85.NewBIP85DRNG(entropy)
key := make([]byte, x25519KeySize)
_, err = drng.Read(key)
if err != nil {
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
}
return key, nil
}
// DeriveEntropyFromXPRV derives 32 bytes of application-scoped entropy from the
// supplied extended private key (xprv) and index n using BIP85.
func DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error) {
// Parse the extended private key
masterKey, err := bip85.ParseMasterKey(xprv)
if err != nil {
return nil, fmt.Errorf("failed to parse master key: %w", err)
}
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
// Derive BIP85 entropy (64 bytes)
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
if err != nil {
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
}
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
drng := bip85.NewBIP85DRNG(entropy)
key := make([]byte, x25519KeySize)
_, err = drng.Read(key)
if err != nil {
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
}
return key, nil
}
// DeriveIdentity is the primary public helper that derives a deterministic
// age identity from a BIP39 mnemonic and index.
func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) {
ent, err := DeriveEntropy(mnemonic, n)
if err != nil {
return nil, err
}
return IdentityFromEntropy(ent)
}
// DeriveIdentityFromXPRV derives a deterministic age identity from an
// extended private key (xprv) and index.
func DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error) {
ent, err := DeriveEntropyFromXPRV(xprv, n)
if err != nil {
return nil, err
}
return IdentityFromEntropy(ent)
}