140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
// 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 (
|
||
"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
|
||
)
|
||
|
||
// 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))
|
||
}
|
||
|
||
// Make a copy to avoid modifying the original
|
||
key := make([]byte, 32)
|
||
copy(key, ent)
|
||
clamp(key)
|
||
|
||
data, err := bech32.ConvertBits(key, 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 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, 32)
|
||
_, 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, 32)
|
||
_, 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)
|
||
}
|