// 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() }