check / check (push) Waiting to run
go-bip39's repository no longer exists. The part of v1.1.0 that secret uses (NewEntropy, NewMnemonic, NewSeed, IsMnemonicValid and what they call; English word list only) now lives in internal/bip39 with upstream's LICENSE beside it, along with upstream's tests of that code and their vectors unchanged. Every import moves there, and the module leaves go.mod and go.sum. Beyond the trimming, only what the linter asked for changed. No derived key or mnemonic changes. Model: opus-5-5
157 lines
4.6 KiB
Go
157 lines
4.6 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 (
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"filippo.io/age"
|
||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||
"github.com/btcsuite/btcd/chaincfg"
|
||
"github.com/btcsuite/btcutil/bech32"
|
||
"sneak.berlin/go/secret/internal/bip39"
|
||
"sneak.berlin/go/secret/pkg/bip85"
|
||
)
|
||
|
||
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)
|
||
}
|