package aezeed import ( "bytes" "math/rand" "testing" "testing/quick" "time" "github.com/stretchr/testify/require" ) // 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) { require.Equal( t, cipherSeed.InternalVersion, cipherSeed2.InternalVersion, "internal version", ) require.Equal(t, cipherSeed.Birthday, cipherSeed2.Birthday, "birthday") require.Equal(t, cipherSeed.Entropy, cipherSeed2.Entropy, "entropy") require.Equal(t, cipherSeed.salt, cipherSeed2.salt, "salt") } // 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) require.NoError(t, 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 = v.salt // Now that the seed has been created, we'll attempt to convert // it to a valid mnemonic. mnemonic, err := cipherSeed.ToMnemonic(v.password) require.NoError(t, err) // Finally we compare the generated mnemonic and birthday to the // expected value. require.Equal(t, v.expectedMnemonic[:], mnemonic[:]) require.Equal(t, v.expectedBirthday, cipherSeed.Birthday) } } // TestWithRandomnessSource tests that seed generation is fully deterministic // when a custom static randomness source is provided. func TestWithRandomnessSource(t *testing.T) { sourceData := append([]byte{}, testEntropy[:]...) sourceData = append(sourceData, testSalt[:]...) src := bytes.NewReader(sourceData) // First, we create new cipher seed with the given values from the test // vector but with no entropy. v := version0TestVectors[0] cipherSeed, err := New( v.version, nil, v.time, WithRandomnessSource(src), ) require.NoError(t, err) // The salt should be set to our test salt. require.Equal(t, testSalt, cipherSeed.salt) // Now that the seed has been created, we'll attempt to convert it to a // valid mnemonic. mnemonic, err := cipherSeed.ToMnemonic(v.password) require.NoError(t, err) // Finally, we compare the generated mnemonic and birthday to the // expected value. require.Equal(t, v.expectedMnemonic[:], mnemonic[:]) require.Equal(t, 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()) require.NoError(t, err) // Now that the seed has been created, we'll attempt to convert it to a // valid mnemonic. mnemonic, err := cipherSeed.ToMnemonic(pass) require.NoError(t, err) // Next, we'll try to decrypt the mnemonic with the passphrase that we // used. cipherSeed2, err := mnemonic.ToCipherSeed(pass) require.NoError(t, 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()) require.NoError(t, err) // Now that the seed has been created, we'll attempt to convert it to a // valid mnemonic. mnemonic, err := cipherSeed.ToMnemonic(pass) require.NoError(t, err) // Next, we'll try to decrypt the mnemonic with the passphrase that we // used. cipherSeed2, err := mnemonic.ToCipherSeed(pass) require.NoError(t, 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 passphrase 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()) require.NoError(t, 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) require.NoError(t, err) // If we try to decipher with the wrong passphrase, we should get the // proper error. wrongPass := []byte("kek") _, err = mnemonic.ToCipherSeed(wrongPass) require.Equal(t, ErrInvalidPass, 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()) require.NoError(t, err) // With the cipher seed obtained, we'll now use the raw encipher method // to obtain our final cipher text. cipherText, err := cipherSeed.Encipher(pass) require.NoError(t, err) mnemonic, err := cipherTextToMnemonic(cipherText) require.NoError(t, err) // Now that we have the ciphertext (mapped to the mnemonic), we'll // attempt to decipher it raw using the user's passphrase. plainSeedBytes, salt, err := mnemonic.Decipher(pass) require.NoError(t, err) require.Equal(t, cipherSeed.salt, salt) // If we deserialize the plaintext seed bytes, it should exactly match // the original cipher seed. newSeed := CipherSeed{ salt: salt, } err = newSeed.decode(bytes.NewReader(plainSeedBytes[:])) require.NoError(t, 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()) require.NoError(t, err) // With the cipher seed obtained, we'll now use the raw encipher method // to obtain our final cipher text. pass := []byte("newpasswhodis") cipherText, err := cipherSeed.Encipher(pass) require.NoError(t, 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")) require.Equal(t, ErrIncorrectVersion, 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()) require.NoError(t, 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) require.NoError(t, 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) require.NoError(t, 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) require.NoError(t, 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 // cipher seed, 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()) require.NoError(t, 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) require.NoError(t, 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) require.Equal(t, ErrInvalidPass, 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 cipher seed // 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 cipher seed. 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 and decode // it precisely. mainScenario := func(version uint8, nowInt int64, entropy [EntropySize]byte) bool { now := time.Unix(nowInt, 0) day := time.Hour * 24 numDaysSinceGenesis := now.Sub(BitcoinGenesisDate) / day seed := CipherSeed{ InternalVersion: version, Birthday: uint16(numDaysSinceGenesis), 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) } } // TestDecipherUnknownMnemonicWord tests that if we obtain a mnemonic, then // 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 TestDecipherUnknownMnemonicWord(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()) require.NoError(t, 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) require.NoError(t, 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 ErrUnknownMnemonicWord. _, err = mnemonic.ToCipherSeed(pass) wordErr := &ErrUnknownMnemonicWord{} require.ErrorAs(t, err, wordErr) require.Equal(t, "kek", wordErr.Word) require.Equal(t, uint8(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 ErrUnknownMnemonicWord. _, err = mnemonic.ToCipherSeed(pass) require.ErrorAs(t, err, wordErr) } // TestDecipherIncorrectMnemonic tests that if we obtain a cipher seed, 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()) require.NoError(t, 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) require.NoError(t, 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 the words happen to be the same by pure chance, we'll try again // with different indexes. if mnemonic[swapIndex1] == mnemonic[swapIndex2] { swapIndex1 = 3 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 ErrIncorrectMnemonic. _, err = mnemonic.ToCipherSeed(pass) require.Equal(t, ErrIncorrectMnemonic, err) } // 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 }