// Package cbk derives NaCl crypto_box keypairs (Curve25519, XSalsa20, // Poly1305) deterministically from BIP39 mnemonic phrases, and seals and // opens messages with them. // // # Key derivation // // 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. // // # Sealing // // Seal and Open wrap box.Seal and box.Open. A sealed message is the // 24-byte random nonce followed by the box ciphertext, so it can be stored // or transmitted as a single opaque byte string. package cbk import ( "bytes" "crypto/hkdf" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "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 // NonceSize is the length in bytes of the nonce that prefixes every // sealed message. NonceSize = 24 // 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" hexKeyLength = KeySize * 2 ) var ( // ErrInvalidKeyHex is returned by KeyFromHex for input that is not // exactly 64 hexadecimal characters. ErrInvalidKeyHex = errors.New("cbk: key must be 64 hexadecimal characters") // ErrSealedTooShort is returned by Open when the input is shorter than // the nonce that must prefix it. ErrSealedTooShort = errors.New("cbk: sealed message is shorter than a nonce") // ErrOpenFailed is returned by Open when authentication fails: the // message was sealed for a different keypair, by a different sender, // or has been modified. ErrOpenFailed = errors.New("cbk: open failed: wrong keys or modified message") ) // CryptoBoxKey is a 32-byte crypto_box key, either public or private. type CryptoBoxKey [KeySize]byte // KeyFromHex parses a key from its 64-character hexadecimal encoding, as // produced by Hex. func KeyFromHex(encoded string) (CryptoBoxKey, error) { var key CryptoBoxKey if len(encoded) != hexKeyLength { return key, fmt.Errorf( "%w: got %d characters", ErrInvalidKeyHex, len(encoded), ) } decoded, err := hex.DecodeString(encoded) if err != nil { return key, fmt.Errorf("%w: %w", ErrInvalidKeyHex, err) } copy(key[:], decoded) return key, nil } // 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}) } // 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() } // Seal encrypts and authenticates message from this keypair to recipient. // The result is a fresh random nonce followed by the ciphertext, so it is // NonceSize + len(message) + box.Overhead bytes long. Only the holder of // recipient's private key, knowing this keypair's public key, can Open it. func (kp CryptoBoxKeypair) Seal( message []byte, recipient CryptoBoxKey, ) ([]byte, error) { var nonce [NonceSize]byte _, err := rand.Read(nonce[:]) if err != nil { return nil, fmt.Errorf("cbk: generating nonce: %w", err) } peersPublicKey := [KeySize]byte(recipient) privateKey := [KeySize]byte(kp.PrivateKey) return box.Seal(nonce[:], message, &nonce, &peersPublicKey, &privateKey), nil } // Open authenticates and decrypts a message that sender produced with Seal // for this keypair. It returns ErrSealedTooShort if sealed cannot contain a // nonce and ErrOpenFailed if authentication fails. func (kp CryptoBoxKeypair) Open( sealed []byte, sender CryptoBoxKey, ) ([]byte, error) { if len(sealed) < NonceSize { return nil, ErrSealedTooShort } var nonce [NonceSize]byte copy(nonce[:], sealed[:NonceSize]) peersPublicKey := [KeySize]byte(sender) privateKey := [KeySize]byte(kp.PrivateKey) ciphertext := sealed[NonceSize:] message, ok := box.Open(nil, ciphertext, &nonce, &peersPublicKey, &privateKey) if !ok { return nil, ErrOpenFailed } return message, nil }