Narrow pkg/cbk to keypair derivation
Remove KeyFromHex, Seal, and Open with their errors and constants. The library only derives crypto_box keypairs from BIP39 mnemonics; callers use golang.org/x/crypto/nacl/box directly for everything else.
This commit is contained in:
@@ -67,29 +67,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The same mnemonic (and optional passphrase) always yields the same
|
// The same mnemonic (and optional passphrase) always yields the same
|
||||||
// keypair, on any machine.
|
// keypair, on any machine. The keys are plain crypto_box keys for use
|
||||||
alice, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
|
// with golang.org/x/crypto/nacl/box.
|
||||||
|
keypair, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bob, err := cbk.FromMnemonic("abandon abandon abandon abandon abandon " +
|
fmt.Println(keypair.PublicKeyHex())
|
||||||
"abandon abandon abandon abandon abandon abandon about")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed, err := alice.Seal([]byte("hello bob"), bob.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
plain, err := bob.Open(sealed, alice.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(string(plain), alice.PublicKeyHex())
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -143,9 +128,9 @@ directory is never read.
|
|||||||
|
|
||||||
### `pkg/cbk`
|
### `pkg/cbk`
|
||||||
|
|
||||||
Derives NaCl crypto_box keypairs (Curve25519, XSalsa20, Poly1305)
|
Derives NaCl crypto_box keypairs (Curve25519) deterministically from BIP39
|
||||||
deterministically from BIP39 mnemonics, and seals and opens messages with them.
|
mnemonics, and nothing else. The derivation is fixed and documented so keys are
|
||||||
The derivation is fixed and documented so keys are reproducible:
|
reproducible:
|
||||||
|
|
||||||
1. The mnemonic is validated and canonicalized with
|
1. The mnemonic is validated and canonicalized with
|
||||||
`bip39wordlist.NormalizeWordlist`.
|
`bip39wordlist.NormalizeWordlist`.
|
||||||
@@ -156,16 +141,13 @@ The derivation is fixed and documented so keys are reproducible:
|
|||||||
`HKDF-SHA256(seed, salt = nil, info = cbk.DerivationInfo)`.
|
`HKDF-SHA256(seed, salt = nil, info = cbk.DerivationInfo)`.
|
||||||
4. The public key is the Curve25519 base-point multiple of the private key.
|
4. The public key is the Curve25519 base-point multiple of the private key.
|
||||||
|
|
||||||
A sealed message is a 24-byte random nonce followed by the box ciphertext, so it
|
The resulting keys are plain 32-byte crypto_box keys for use with
|
||||||
can be handled as a single opaque byte string. The API:
|
`golang.org/x/crypto/nacl/box`. The API:
|
||||||
|
|
||||||
- `cbk.New(cbk.Params{Mnemonic, Passphrase})` and `cbk.FromMnemonic(m)` return a
|
- `cbk.New(cbk.Params{Mnemonic, Passphrase})` and `cbk.FromMnemonic(m)` return a
|
||||||
`CryptoBoxKeypair`.
|
`CryptoBoxKeypair`.
|
||||||
- `CryptoBoxKeypair.Seal(message, recipientPublicKey)` and
|
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, and the `PublicKeyHex`,
|
||||||
`CryptoBoxKeypair.Open(sealed, senderPublicKey)`.
|
`PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors on the keypair.
|
||||||
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, `cbk.KeyFromHex(s)`, and the
|
|
||||||
`PublicKeyHex`, `PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors
|
|
||||||
on the keypair.
|
|
||||||
|
|
||||||
The test suite pins the keypair derived from the standard all-zero-entropy BIP39
|
The test suite pins the keypair derived from the standard all-zero-entropy BIP39
|
||||||
test vector. If that test ever fails, the derivation has changed and existing
|
test vector. If that test ever fails, the derivation has changed and existing
|
||||||
|
|||||||
+4
-102
@@ -1,8 +1,6 @@
|
|||||||
// Package cbk derives NaCl crypto_box keypairs (Curve25519, XSalsa20,
|
// Package cbk derives NaCl crypto_box keypairs (Curve25519) deterministically
|
||||||
// Poly1305) deterministically from BIP39 mnemonic phrases, and seals and
|
// from BIP39 mnemonic phrases. That is all it does; the resulting keys are
|
||||||
// opens messages with them.
|
// plain 32-byte crypto_box keys for use with golang.org/x/crypto/nacl/box.
|
||||||
//
|
|
||||||
// # Key derivation
|
|
||||||
//
|
//
|
||||||
// The same mnemonic and passphrase always produce the same keypair:
|
// The same mnemonic and passphrase always produce the same keypair:
|
||||||
//
|
//
|
||||||
@@ -16,21 +14,13 @@
|
|||||||
// HKDF-SHA256, using no salt and DerivationInfo as the info string.
|
// HKDF-SHA256, using no salt and DerivationInfo as the info string.
|
||||||
// 4. The public key is the Curve25519 scalar multiplication of the
|
// 4. The public key is the Curve25519 scalar multiplication of the
|
||||||
// private key with the base point, via golang.org/x/crypto/nacl/box.
|
// 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
|
package cbk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hkdf"
|
"crypto/hkdf"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/tyler-smith/go-bip39"
|
"github.com/tyler-smith/go-bip39"
|
||||||
@@ -43,56 +33,14 @@ const (
|
|||||||
// KeySize is the length in bytes of a crypto_box public or private key.
|
// KeySize is the length in bytes of a crypto_box public or private key.
|
||||||
KeySize = 32
|
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
|
// DerivationInfo is the HKDF info string that binds derived keys to
|
||||||
// this package. Changing it changes every derived key.
|
// this package. Changing it changes every derived key.
|
||||||
DerivationInfo = "sneak.berlin/go/x/pkg/cbk crypto_box v1"
|
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.
|
// CryptoBoxKey is a 32-byte crypto_box key, either public or private.
|
||||||
type CryptoBoxKey [KeySize]byte
|
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.
|
// Bytes returns a copy of the key as a byte slice.
|
||||||
func (k CryptoBoxKey) Bytes() []byte {
|
func (k CryptoBoxKey) Bytes() []byte {
|
||||||
return k[:]
|
return k[:]
|
||||||
@@ -151,7 +99,7 @@ func New(p Params) (CryptoBoxKeypair, error) {
|
|||||||
// FromMnemonic derives the keypair for mnemonic with an empty passphrase.
|
// FromMnemonic derives the keypair for mnemonic with an empty passphrase.
|
||||||
// It is shorthand for New(Params{Mnemonic: mnemonic}).
|
// It is shorthand for New(Params{Mnemonic: mnemonic}).
|
||||||
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
|
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
|
||||||
return New(Params{Mnemonic: mnemonic})
|
return New(Params{Mnemonic: mnemonic, Passphrase: ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicKeyBytes returns a copy of the public key as a byte slice.
|
// PublicKeyBytes returns a copy of the public key as a byte slice.
|
||||||
@@ -175,49 +123,3 @@ func (kp CryptoBoxKeypair) PublicKeyHex() string {
|
|||||||
func (kp CryptoBoxKeypair) PrivateKeyHex() string {
|
func (kp CryptoBoxKeypair) PrivateKeyHex() string {
|
||||||
return kp.PrivateKey.Hex()
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-96
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/crypto/nacl/box"
|
|
||||||
|
|
||||||
"sneak.berlin/go/x/pkg/bip39wordlist"
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
"sneak.berlin/go/x/pkg/cbk"
|
"sneak.berlin/go/x/pkg/cbk"
|
||||||
@@ -83,6 +82,25 @@ func TestNewPassphraseChangesKeys(t *testing.T) {
|
|||||||
assert.Equal(t, withExtraWord, again, "derivation is deterministic")
|
assert.Equal(t, withExtraWord, again, "derivation is deterministic")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewDifferentMnemonicsGiveDifferentKeys(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
second, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
firstKeypair, err := cbk.FromMnemonic(first)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
secondKeypair, err := cbk.FromMnemonic(second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEqual(t, firstKeypair.PublicKey, secondKeypair.PublicKey)
|
||||||
|
assert.NotEqual(t, firstKeypair.PrivateKey, secondKeypair.PrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -105,99 +123,5 @@ func TestKeyAccessors(t *testing.T) {
|
|||||||
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
|
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
|
||||||
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
|
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
|
||||||
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
|
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
|
||||||
}
|
assert.Len(t, keypair.PublicKeyHex(), 2*cbk.KeySize)
|
||||||
|
|
||||||
func TestKeyFromHexRoundTrip(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
keypair, err := cbk.FromMnemonic(vectorMnemonic())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
parsed, err := cbk.KeyFromHex(keypair.PublicKeyHex())
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, keypair.PublicKey, parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyFromHexRejectsBadInput(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, err := cbk.KeyFromHex("abcd")
|
|
||||||
require.ErrorIs(t, err, cbk.ErrInvalidKeyHex)
|
|
||||||
|
|
||||||
notHex := "zz" + keypairForTest(t).PublicKeyHex()[2:]
|
|
||||||
|
|
||||||
_, err = cbk.KeyFromHex(notHex)
|
|
||||||
require.ErrorIs(t, err, cbk.ErrInvalidKeyHex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSealOpenRoundTrip(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
alice := keypairForTest(t)
|
|
||||||
bob := keypairForTest(t)
|
|
||||||
message := []byte("the quick brown fox jumps over the lazy dog")
|
|
||||||
|
|
||||||
sealed, err := alice.Seal(message, bob.PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, sealed, cbk.NonceSize+len(message)+box.Overhead)
|
|
||||||
|
|
||||||
opened, err := bob.Open(sealed, alice.PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, message, opened)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSealUsesFreshNonces(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
alice := keypairForTest(t)
|
|
||||||
bob := keypairForTest(t)
|
|
||||||
message := []byte("same message twice")
|
|
||||||
|
|
||||||
first, err := alice.Seal(message, bob.PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
second, err := alice.Seal(message, bob.PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.NotEqual(t, first, second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpenRejectsTamperingAndWrongKeys(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
alice := keypairForTest(t)
|
|
||||||
bob := keypairForTest(t)
|
|
||||||
eve := keypairForTest(t)
|
|
||||||
message := []byte("for bob only")
|
|
||||||
|
|
||||||
sealed, err := alice.Seal(message, bob.PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = eve.Open(sealed, alice.PublicKey)
|
|
||||||
require.ErrorIs(t, err, cbk.ErrOpenFailed, "wrong recipient")
|
|
||||||
|
|
||||||
_, err = bob.Open(sealed, eve.PublicKey)
|
|
||||||
require.ErrorIs(t, err, cbk.ErrOpenFailed, "wrong sender")
|
|
||||||
|
|
||||||
tampered := append([]byte(nil), sealed...)
|
|
||||||
tampered[len(tampered)-1] ^= 0x01
|
|
||||||
|
|
||||||
_, err = bob.Open(tampered, alice.PublicKey)
|
|
||||||
require.ErrorIs(t, err, cbk.ErrOpenFailed, "modified ciphertext")
|
|
||||||
|
|
||||||
_, err = bob.Open(sealed[:cbk.NonceSize-1], alice.PublicKey)
|
|
||||||
require.ErrorIs(t, err, cbk.ErrSealedTooShort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keypairForTest derives a keypair from a freshly generated mnemonic.
|
|
||||||
func keypairForTest(t *testing.T) cbk.CryptoBoxKeypair {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
keypair, err := cbk.FromMnemonic(mnemonic)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return keypair
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user