diff --git a/aezeed/cipherseeed_test.go b/aezeed/cipherseeed_test.go new file mode 100644 index 00000000..a6215059 --- /dev/null +++ b/aezeed/cipherseeed_test.go @@ -0,0 +1,513 @@ +package aezeed + +import ( + "bytes" + "math/rand" + "testing" + "testing/quick" + "time" +) + +var ( + testEntropy = [EntropySize]byte{ + 0x81, 0xb6, 0x37, 0xd8, + 0x63, 0x59, 0xe6, 0x96, + 0x0d, 0xe7, 0x95, 0xe4, + 0x1e, 0x0b, 0x4c, 0xfd, + } +) + +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[:]) + } +} + +func TestAezeedVersion0TestVectors(t *testing.T) { + t.Parallel() + + // TODO(roasbeef): +} + +// 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) + } +} + +// 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 +}