// Package agehd derives deterministic X25519 age identities from a // BIP-39 seed using a vendor/application-scoped BIP-85 path: // // m / 83696968′ / ′ / ′ / 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 ( "fmt" "strings" "filippo.io/age" bip85 "github.com/bitmask-dev/go-bip85" "github.com/btcsuite/btcutil/bech32" "github.com/tyler-smith/go-bip32" "github.com/tyler-smith/go-bip39" ) const ( purpose = uint32(83696968) // fixed by BIP-85 (“bip”) vendorID = uint32(592366788) // berlin.sneak appID = uint32(733482323) // secret firstH = bip32.FirstHardenedChild hrp = "age-secret-key-" // Bech32 HRP used by age ) // 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) != 32 { return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent)) } clamp(ent) data, err := bech32.ConvertBits(ent, 8, 5, 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. func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) { seed := bip39.NewSeed(mnemonic, "") root, err := bip32.NewMasterKey(seed) if err != nil { return nil, err } // m / purpose′ / vendor′ purp, err := root.NewChildKey(purpose + firstH) if err != nil { return nil, err } vend, err := purp.NewChildKey(vendorID + firstH) if err != nil { return nil, err } return bip85.NewFromBip32(vend).DeriveRawEntropy(appID, 32, n) } // DeriveIdentity is the primary public helper. func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) { ent, err := DeriveEntropy(mnemonic, n) if err != nil { return nil, err } return IdentityFromEntropy(ent) }