// Package cbk derives NaCl crypto_box keypairs (Curve25519) deterministically // from BIP39 mnemonic phrases. 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 and passphrase 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 BIP39 wallet // would. // 3. The 32-byte crypto_box private key is derived from that seed with // HKDF-SHA256, using no salt and DerivationInfo as the info string. // 4. The public key is the Curve25519 scalar multiplication of the // private key with the base point, via golang.org/x/crypto/nacl/box. package cbk import ( "bytes" "crypto/hkdf" "crypto/sha256" "encoding/hex" "fmt" "github.com/tyler-smith/go-bip39" "golang.org/x/crypto/nacl/box" "sneak.berlin/go/x/pkg/bip39wordlist" ) const ( // KeySize is the length in bytes of a crypto_box public or private key. KeySize = 32 // DerivationInfo is the HKDF info string that binds derived keys to // this package. Changing it changes every derived key. DerivationInfo = "sneak.berlin/go/x/pkg/cbk crypto_box v1" ) // 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 } // CryptoBoxKeypair is a crypto_box public and private key pair. type CryptoBoxKeypair struct { PublicKey CryptoBoxKey PrivateKey CryptoBoxKey } // New derives the keypair for the mnemonic and passphrase 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) { mnemonic, err := bip39wordlist.NormalizeWordlist(p.Mnemonic) if err != nil { return CryptoBoxKeypair{}, err } seed := bip39.NewSeed(mnemonic, p.Passphrase) privateKeyBytes, err := hkdf.Key(sha256.New, seed, nil, DerivationInfo, KeySize) if err != nil { return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving private key: %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. // It is shorthand for New(Params{Mnemonic: mnemonic}). func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) { return New(Params{Mnemonic: mnemonic, Passphrase: ""}) } // 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() }