lnd.xprv/aezeed/cipherseed_test.go
Oliver Gugger 5904efe9ed
aezeed: export wordlist and properties
To make it possible to use the wordlist used for aezeed outside of the
aezeed package we export certain properties of the word list and the
word list itself.
2021-06-30 14:20:32 +02:00

612 lines
19 KiB
Go

package aezeed
import (
"bytes"
"math/rand"
"testing"
"testing/quick"
"time"
)
// TestVector defines the values that are used to create a fully initialized
// aezeed mnemonic seed and the expected values that should be calculated.
type TestVector struct {
version uint8
time time.Time
entropy [EntropySize]byte
salt [saltSize]byte
password []byte
expectedMnemonic [NumMnemonicWords]string
expectedBirthday uint16
}
var (
testEntropy = [EntropySize]byte{
0x81, 0xb6, 0x37, 0xd8,
0x63, 0x59, 0xe6, 0x96,
0x0d, 0xe7, 0x95, 0xe4,
0x1e, 0x0b, 0x4c, 0xfd,
}
testSalt = [saltSize]byte{
0x73, 0x61, 0x6c, 0x74, 0x31, // equal to "salt1"
}
version0TestVectors = []TestVector{
{
version: 0,
time: BitcoinGenesisDate,
entropy: testEntropy,
salt: testSalt,
password: []byte{},
expectedMnemonic: [NumMnemonicWords]string{
"ability", "liquid", "travel", "stem", "barely", "drastic",
"pact", "cupboard", "apple", "thrive", "morning", "oak",
"feature", "tissue", "couch", "old", "math", "inform",
"success", "suggest", "drink", "motion", "know", "royal",
},
expectedBirthday: 0,
},
{
version: 0,
time: time.Unix(1521799345, 0), // 03/23/2018 @ 10:02am (UTC)
entropy: testEntropy,
salt: testSalt,
password: []byte("!very_safe_55345_password*"),
expectedMnemonic: [NumMnemonicWords]string{
"able", "tree", "stool", "crush", "transfer", "cloud",
"cross", "three", "profit", "outside", "hen", "citizen",
"plate", "ride", "require", "leg", "siren", "drum",
"success", "suggest", "drink", "require", "fiscal", "upgrade",
},
expectedBirthday: 3365,
},
}
)
func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed,
cipherSeed2 *CipherSeed) {
if cipherSeed.InternalVersion != cipherSeed2.InternalVersion {
t.Fatalf("mismatched versions: expected %v, got %v",
cipherSeed.InternalVersion, cipherSeed2.InternalVersion)
}
if cipherSeed.Birthday != cipherSeed2.Birthday {
t.Fatalf("mismatched birthday: expected %v, got %v",
cipherSeed.Birthday, cipherSeed2.Birthday)
}
if cipherSeed.Entropy != cipherSeed2.Entropy {
t.Fatalf("mismatched versions: expected %x, got %x",
cipherSeed.Entropy[:], cipherSeed2.Entropy[:])
}
}
// TestAezeedVersion0TestVectors tests some fixed test vector values against
// the expected mnemonic words.
func TestAezeedVersion0TestVectors(t *testing.T) {
t.Parallel()
// To minimize the number of tests that need to be run,
// go through all test vectors in the same test and also check
// the birthday calculation while we're at it.
for _, v := range version0TestVectors {
// First, we create new cipher seed with the given values
// from the test vector.
cipherSeed, err := New(v.version, &v.entropy, v.time)
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Then we need to set the salt to the pre-defined value, otherwise
// we'll end up with randomness in our mnemonics.
cipherSeed.salt = testSalt
// Now that the seed has been created, we'll attempt to convert it to a
// valid mnemonic.
mnemonic, err := cipherSeed.ToMnemonic(v.password)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Finally we compare the generated mnemonic and birthday to the
// expected value.
if mnemonic != v.expectedMnemonic {
t.Fatalf("mismatched mnemonic: expected %s, got %s",
v.expectedMnemonic, mnemonic)
}
if cipherSeed.Birthday != v.expectedBirthday {
t.Fatalf("mismatched birthday: expected %v, got %v",
v.expectedBirthday, cipherSeed.Birthday)
}
}
}
// TestEmptyPassphraseDerivation tests that the aezeed scheme is able to derive
// a proper mnemonic, and decipher that mnemonic when the user uses an empty
// passphrase.
func TestEmptyPassphraseDerivation(t *testing.T) {
t.Parallel()
// Our empty passphrase...
pass := []byte{}
// We'll now create a new cipher seed with an internal version of zero
// to simulate a wallet that just adopted the scheme.
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that the seed has been created, we'll attempt to convert it to a
// valid mnemonic.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Next, we'll try to decrypt the mnemonic with the passphrase that we
// used.
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
if err != nil {
t.Fatalf("unable to decrypt mnemonic: %v", err)
}
// Finally, we'll ensure that the uncovered cipher seed matches
// precisely.
assertCipherSeedEqual(t, cipherSeed, cipherSeed2)
}
// TestManualEntropyGeneration tests that if the user doesn't provide a source
// of entropy, then we do so ourselves.
func TestManualEntropyGeneration(t *testing.T) {
t.Parallel()
// Our empty passphrase...
pass := []byte{}
// We'll now create a new cipher seed with an internal version of zero
// to simulate a wallet that just adopted the scheme.
cipherSeed, err := New(0, nil, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that the seed has been created, we'll attempt to convert it to a
// valid mnemonic.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Next, we'll try to decrypt the mnemonic with the passphrase that we
// used.
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
if err != nil {
t.Fatalf("unable to decrypt mnemonic: %v", err)
}
// Finally, we'll ensure that the uncovered cipher seed matches
// precisely.
assertCipherSeedEqual(t, cipherSeed, cipherSeed2)
}
// TestInvalidPassphraseRejection tests if a caller attempts to use the
// incorrect passprhase for an enciphered seed, then the proper error is
// returned.
func TestInvalidPassphraseRejection(t *testing.T) {
t.Parallel()
// First, we'll generate a new cipher seed with a test passphrase.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that we have our cipher seed, we'll encipher it and request a
// mnemonic that we can use to recover later.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// If we try to decipher with the wrong passphrase, we should get the
// proper error.
wrongPass := []byte("kek")
if _, err := mnemonic.ToCipherSeed(wrongPass); err != ErrInvalidPass {
t.Fatalf("expected ErrInvalidPass, instead got %v", err)
}
}
// TestRawEncipherDecipher tests that callers are able to use the raw methods
// to map between ciphertext and the raw plaintext deciphered seed.
func TestRawEncipherDecipher(t *testing.T) {
t.Parallel()
// First, we'll generate a new cipher seed with a test passphrase.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// With the cipherseed obtained, we'll now use the raw encipher method
// to obtain our final cipher text.
cipherText, err := cipherSeed.Encipher(pass)
if err != nil {
t.Fatalf("unable to encipher seed: %v", err)
}
mnemonic, err := cipherTextToMnemonic(cipherText)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Now that we have the ciphertext (mapped to the mnemonic), we'll
// attempt to decipher it raw using the user's passphrase.
plainSeedBytes, err := mnemonic.Decipher(pass)
if err != nil {
t.Fatalf("unable to decipher: %v", err)
}
// If we deserialize the plaintext seed bytes, it should exactly match
// the original cipher seed.
var newSeed CipherSeed
err = newSeed.decode(bytes.NewReader(plainSeedBytes[:]))
if err != nil {
t.Fatalf("unable to decode cipher seed: %v", err)
}
assertCipherSeedEqual(t, cipherSeed, &newSeed)
}
// TestInvalidExternalVersion tests that if we present a ciphertext with the
// incorrect version to decipherCipherSeed, then it fails with the expected
// error.
func TestInvalidExternalVersion(t *testing.T) {
t.Parallel()
// First, we'll generate a new cipher seed.
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// With the cipherseed obtained, we'll now use the raw encipher method
// to obtain our final cipher text.
pass := []byte("newpasswhodis")
cipherText, err := cipherSeed.Encipher(pass)
if err != nil {
t.Fatalf("unable to encipher seed: %v", err)
}
// Now that we have the cipher text, we'll modify the first byte to be
// an invalid version.
cipherText[0] = 44
// With the version swapped, if we try to decipher it, (no matter the
// passphrase), it should fail.
_, err = decipherCipherSeed(cipherText, []byte("kek"))
if err != ErrIncorrectVersion {
t.Fatalf("wrong error: expected ErrIncorrectVersion, "+
"got %v", err)
}
}
// TestChangePassphrase tests that we're able to generate a cipher seed, then
// change the password. If we attempt to decipher the new enciphered seed, then
// we should get the exact same seed back.
func TestChangePassphrase(t *testing.T) {
t.Parallel()
// First, we'll generate a new cipher seed with a test passphrase.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that we have our cipher seed, we'll encipher it and request a
// mnemonic that we can use to recover later.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Now that have the mnemonic, we'll attempt to re-encipher the
// passphrase in order to get a brand new mnemonic.
newPass := []byte("strongerpassyeh!")
newmnemonic, err := mnemonic.ChangePass(pass, newPass)
if err != nil {
t.Fatalf("unable to change passphrase: %v", err)
}
// We'll now attempt to decipher the new mnemonic using the new
// passphrase to arrive at (what should be) the original cipher seed.
newCipherSeed, err := newmnemonic.ToCipherSeed(newPass)
if err != nil {
t.Fatalf("unable to decipher cipher seed: %v", err)
}
// Now that we have the cipher seed, we'll verify that the plaintext
// seed matches *identically*.
assertCipherSeedEqual(t, cipherSeed, newCipherSeed)
}
// TestChangePassphraseWrongPass tests that if we have a valid enciphered
// cipherseed, but then try to change the password with the *wrong* password,
// then we get an error.
func TestChangePassphraseWrongPass(t *testing.T) {
t.Parallel()
// First, we'll generate a new cipher seed with a test passphrase.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that we have our cipher seed, we'll encipher it and request a
// mnemonic that we can use to recover later.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Now that have the mnemonic, we'll attempt to re-encipher the
// passphrase in order to get a brand new mnemonic. However, we'll be
// using the *wrong* passphrase. This should result in an
// ErrInvalidPass error.
wrongPass := []byte("kek")
newPass := []byte("strongerpassyeh!")
_, err = mnemonic.ChangePass(wrongPass, newPass)
if err != ErrInvalidPass {
t.Fatalf("expected ErrInvalidPass, instead got %v", err)
}
}
// TestMnemonicEncoding uses quickcheck like property based testing to ensure
// that we're always able to fully recover the original byte stream encoded
// into the mnemonic phrase.
func TestMnemonicEncoding(t *testing.T) {
t.Parallel()
// mainScenario is the main driver of our property based test. We'll
// ensure that given a random byte string of length 33 bytes, if we
// convert that to the mnemonic, then we should be able to reverse the
// conversion.
mainScenario := func(cipherSeedBytes [EncipheredCipherSeedSize]byte) bool {
mnemonic, err := cipherTextToMnemonic(cipherSeedBytes)
if err != nil {
t.Fatalf("unable to map cipher text: %v", err)
return false
}
newCipher := mnemonicToCipherText(&mnemonic)
if newCipher != cipherSeedBytes {
t.Fatalf("cipherseed doesn't match: expected %v, got %v",
cipherSeedBytes, newCipher)
return false
}
return true
}
if err := quick.Check(mainScenario, nil); err != nil {
t.Fatalf("fuzz check failed: %v", err)
}
}
// TestEncipherDecipher is a property-based test that ensures that given a
// version, entropy, and birthday, then we're able to map that to a cipherseed
// mnemonic, then back to the original plaintext cipher seed.
func TestEncipherDecipher(t *testing.T) {
t.Parallel()
// mainScenario is the main driver of our property based test. We'll
// ensure that given a random seed tuple (internal version, entropy,
// and birthday) we're able to convert that to a valid cipher seed.
// Additionally, we should be able to decipher the final mnemonic, and
// recover the original cipherseed.
mainScenario := func(version uint8, entropy [EntropySize]byte,
nowInt int64, pass [20]byte) bool {
now := time.Unix(nowInt, 0)
cipherSeed, err := New(version, &entropy, now)
if err != nil {
t.Fatalf("unable to map cipher text: %v", err)
return false
}
mnemonic, err := cipherSeed.ToMnemonic(pass[:])
if err != nil {
t.Fatalf("unable to generate mnemonic: %v", err)
return false
}
cipherSeed2, err := mnemonic.ToCipherSeed(pass[:])
if err != nil {
t.Fatalf("unable to decrypt cipher seed: %v", err)
return false
}
if cipherSeed.InternalVersion != cipherSeed2.InternalVersion {
t.Fatalf("mismatched versions: expected %v, got %v",
cipherSeed.InternalVersion, cipherSeed2.InternalVersion)
return false
}
if cipherSeed.Birthday != cipherSeed2.Birthday {
t.Fatalf("mismatched birthday: expected %v, got %v",
cipherSeed.Birthday, cipherSeed2.Birthday)
return false
}
if cipherSeed.Entropy != cipherSeed2.Entropy {
t.Fatalf("mismatched versions: expected %x, got %x",
cipherSeed.Entropy[:], cipherSeed2.Entropy[:])
return false
}
return true
}
if err := quick.Check(mainScenario, nil); err != nil {
t.Fatalf("fuzz check failed: %v", err)
}
}
// TestSeedEncodeDecode tests that we're able to reverse the encoding of an
// arbitrary raw seed.
func TestSeedEncodeDecode(t *testing.T) {
// mainScenario is the primary driver of our property-based test. We'll
// ensure that given a random cipher seed, we can encode it an decode
// it precisely.
mainScenario := func(version uint8, nowInt int64,
entropy [EntropySize]byte) bool {
now := time.Unix(nowInt, 0)
seed := CipherSeed{
InternalVersion: version,
Birthday: uint16(now.Sub(BitcoinGenesisDate) / (time.Hour * 24)),
Entropy: entropy,
}
var b bytes.Buffer
if err := seed.encode(&b); err != nil {
t.Fatalf("unable to encode: %v", err)
return false
}
var newSeed CipherSeed
if err := newSeed.decode(&b); err != nil {
t.Fatalf("unable to decode: %v", err)
return false
}
if seed.InternalVersion != newSeed.InternalVersion {
t.Fatalf("mismatched versions: expected %v, got %v",
seed.InternalVersion, newSeed.InternalVersion)
return false
}
if seed.Birthday != newSeed.Birthday {
t.Fatalf("mismatched birthday: expected %v, got %v",
seed.Birthday, newSeed.Birthday)
return false
}
if seed.Entropy != newSeed.Entropy {
t.Fatalf("mismatched versions: expected %x, got %x",
seed.Entropy[:], newSeed.Entropy[:])
return false
}
return true
}
if err := quick.Check(mainScenario, nil); err != nil {
t.Fatalf("fuzz check failed: %v", err)
}
}
// TestDecipherUnknownMnenomicWord tests that if we obtain a mnemonic, the
// modify one of the words to not be within the word list, then it's detected
// when we attempt to map it back to the original cipher seed.
func TestDecipherUnknownMnenomicWord(t *testing.T) {
t.Parallel()
// First, we'll create a new cipher seed with "test" ass a password.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that we have our cipher seed, we'll encipher it and request a
// mnemonic that we can use to recover later.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// Before we attempt to decrypt the cipher seed, we'll mutate one of
// the word so it isn't actually in our final word list.
randIndex := rand.Int31n(int32(len(mnemonic)))
mnemonic[randIndex] = "kek"
// If we attempt to map back to the original cipher seed now, then we
// should get ErrUnknownMnenomicWord.
_, err = mnemonic.ToCipherSeed(pass)
if err == nil {
t.Fatalf("expected ErrUnknownMnenomicWord error")
}
wordErr, ok := err.(ErrUnknownMnenomicWord)
if !ok {
t.Fatalf("expected ErrUnknownMnenomicWord instead got %T", err)
}
if wordErr.Word != "kek" {
t.Fatalf("word mismatch: expected %v, got %v", "kek", wordErr.Word)
}
if int32(wordErr.Index) != randIndex {
t.Fatalf("wrong index detected: expected %v, got %v",
randIndex, wordErr.Index)
}
// If the mnemonic includes a word that is not in the englishList
// it fails, even when it is a substring of a valid word
// Example: `heart` is in the list, `hear` is not
mnemonic[randIndex] = "hear"
// If we attempt to map back to the original cipher seed now, then we
// should get ErrUnknownMnenomicWord.
_, err = mnemonic.ToCipherSeed(pass)
if err == nil {
t.Fatalf("expected ErrUnknownMnenomicWord error")
}
_, ok = err.(ErrUnknownMnenomicWord)
if !ok {
t.Fatalf("expected ErrUnknownMnenomicWord instead got %T", err)
}
}
// TestDecipherIncorrectMnemonic tests that if we obtain a cipherseed, but then
// swap out words, then checksum fails.
func TestDecipherIncorrectMnemonic(t *testing.T) {
// First, we'll create a new cipher seed with "test" ass a password.
pass := []byte("test")
cipherSeed, err := New(0, &testEntropy, time.Now())
if err != nil {
t.Fatalf("unable to create seed: %v", err)
}
// Now that we have our cipher seed, we'll encipher it and request a
// mnemonic that we can use to recover later.
mnemonic, err := cipherSeed.ToMnemonic(pass)
if err != nil {
t.Fatalf("unable to create mnemonic: %v", err)
}
// We'll now swap out two words from the mnemonic, which should trigger
// a checksum failure.
swapIndex1 := 9
swapIndex2 := 13
mnemonic[swapIndex1], mnemonic[swapIndex2] = mnemonic[swapIndex2], mnemonic[swapIndex1]
// If we attempt to decrypt now, we should get a checksum failure.
// If we attempt to map back to the original cipher seed now, then we
// should get ErrUnknownMnenomicWord.
_, err = mnemonic.ToCipherSeed(pass)
if err != ErrIncorrectMnemonic {
t.Fatalf("expected ErrIncorrectMnemonic error")
}
}
// TODO(roasbeef): add test failure checksum fail is modified, new error
func init() {
// For the purposes of our test, we'll crank down the scrypt params a
// bit.
scryptN = 16
scryptR = 8
scryptP = 1
}