Files
x/pkg/cbk/cbk.go
T
sneak fe6dbba428 Derive cbk keys with BIP85 instead of an ad hoc HKDF label
The private key is now the BIP85 HEX application output for 32 bytes at
m/83696968'/128169'/32'/{index}' from the BIP32 master key of the BIP39
seed, via sneak.berlin/go/secret/pkg/bip85, so any BIP85 tool reproduces
it and one mnemonic backs many keypairs through Params.Index. Tests
check the mnemonic-to-master step and the HEX application against the
BIP85 specification vectors and pin the resulting keypair.
2026-10-31 11:38:02 -07:00

160 lines
5.4 KiB
Go

// Package cbk derives NaCl crypto_box keypairs (Curve25519) deterministically
// from BIP39 mnemonic phrases using BIP85. That is all it does; the
// resulting keys are plain 32-byte crypto_box keys for use with
// golang.org/x/crypto/nacl/box.
//
// The same mnemonic, passphrase, and index always produce the same keypair:
//
// 1. The mnemonic is validated and canonicalized with
// bip39wordlist.NormalizeWordlist (lowercase, single spaces).
// 2. The standard 64-byte BIP39 seed is computed from the canonical
// mnemonic and the optional passphrase with PBKDF2-HMAC-SHA512 (2048
// rounds, salt "mnemonic" + passphrase), exactly as a wallet would.
// 3. The seed becomes a BIP32 master key.
// 4. BIP85 (sneak.berlin/go/secret/pkg/bip85) derives 32 bytes of
// application entropy from the master key with the HEX application,
// at path m/83696968'/128169'/32'/{index}'. Those bytes are the
// crypto_box private key.
// 5. The public key is the Curve25519 scalar multiplication of the
// private key with the base point, via golang.org/x/crypto/nacl/box.
//
// Because the entropy comes from BIP85, any BIP85-capable tool given the
// same mnemonic reproduces the private key, and the index lets one
// mnemonic back any number of independent keypairs.
package cbk
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/nacl/box"
"sneak.berlin/go/secret/pkg/bip85"
"sneak.berlin/go/x/pkg/bip39wordlist"
)
const (
// KeySize is the length in bytes of a crypto_box public or private key.
// It is also the byte count requested from the BIP85 HEX application.
KeySize = 32
// MaxIndex is the largest valid Params.Index. BIP85 path components
// are hardened BIP32 child indexes, which leaves 31 bits.
MaxIndex = hdkeychain.HardenedKeyStart - 1
)
// ErrIndexTooLarge is returned by New when Params.Index exceeds MaxIndex.
var ErrIndexTooLarge = errors.New("cbk: index exceeds the BIP32 hardened range")
// CryptoBoxKey is a 32-byte crypto_box key, either public or private.
type CryptoBoxKey [KeySize]byte
// Bytes returns a copy of the key as a byte slice.
func (k CryptoBoxKey) Bytes() []byte {
return k[:]
}
// Hex returns the key as 64 lowercase hexadecimal characters.
func (k CryptoBoxKey) Hex() string {
return hex.EncodeToString(k[:])
}
// Params configures New.
type Params struct {
// Mnemonic is the BIP39 mnemonic phrase. Any letter case and any
// whitespace between words is accepted; it is normalized before use.
Mnemonic string
// Passphrase is the optional BIP39 passphrase (sometimes called the
// "25th word"). It is used verbatim: it is case sensitive and is not
// trimmed. An empty passphrase is the common case.
Passphrase string
// Index selects which of the keypairs derivable from the mnemonic to
// return. It is the final component of the BIP85 derivation path, so
// each index yields an independent keypair. Zero is the first.
Index uint32
}
// CryptoBoxKeypair is a crypto_box public and private key pair.
type CryptoBoxKeypair struct {
PublicKey CryptoBoxKey
PrivateKey CryptoBoxKey
}
// New derives the keypair for the mnemonic, passphrase, and index in p. If
// the mnemonic is not valid it returns an error wrapping one of the
// bip39wordlist sentinel errors.
func New(p Params) (CryptoBoxKeypair, error) {
if p.Index > MaxIndex {
return CryptoBoxKeypair{}, fmt.Errorf("%w: %d", ErrIndexTooLarge, p.Index)
}
mnemonic, err := bip39wordlist.NormalizeWordlist(p.Mnemonic)
if err != nil {
return CryptoBoxKeypair{}, err
}
// The network parameters only choose the version bytes used when a
// key is serialized; they do not affect derivation.
masterKey, err := hdkeychain.NewMaster(
bip39.NewSeed(mnemonic, p.Passphrase), &chaincfg.MainNetParams,
)
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP32 master key: %w", err)
}
privateKeyHex, err := bip85.DeriveHex(masterKey, KeySize, p.Index)
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP85 entropy: %w", err)
}
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: decoding BIP85 entropy: %w", err)
}
publicKey, privateKey, err := box.GenerateKey(bytes.NewReader(privateKeyBytes))
if err != nil {
return CryptoBoxKeypair{}, fmt.Errorf("cbk: computing public key: %w", err)
}
return CryptoBoxKeypair{
PublicKey: CryptoBoxKey(*publicKey),
PrivateKey: CryptoBoxKey(*privateKey),
}, nil
}
// FromMnemonic derives the keypair for mnemonic with an empty passphrase
// at index zero. It is shorthand for New(Params{Mnemonic: mnemonic}).
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
return New(Params{Mnemonic: mnemonic, Passphrase: "", Index: 0})
}
// PublicKeyBytes returns a copy of the public key as a byte slice.
func (kp CryptoBoxKeypair) PublicKeyBytes() []byte {
return kp.PublicKey.Bytes()
}
// PrivateKeyBytes returns a copy of the private key as a byte slice.
func (kp CryptoBoxKeypair) PrivateKeyBytes() []byte {
return kp.PrivateKey.Bytes()
}
// PublicKeyHex returns the public key as 64 lowercase hexadecimal
// characters.
func (kp CryptoBoxKeypair) PublicKeyHex() string {
return kp.PublicKey.Hex()
}
// PrivateKeyHex returns the private key as 64 lowercase hexadecimal
// characters.
func (kp CryptoBoxKeypair) PrivateKeyHex() string {
return kp.PrivateKey.Hex()
}