diff --git a/aezeed/cipherseed.go b/aezeed/cipherseed.go index a55bb5d3e..4e05b25e0 100644 --- a/aezeed/cipherseed.go +++ b/aezeed/cipherseed.go @@ -62,7 +62,7 @@ const ( // be seen as the size of the equivalent MAC. CipherTextExpansion = 4 - // EntropySize is the number of bytes of entropy we'll use the generate + // EntropySize is the number of bytes of entropy we'll use to generate // the seed. EntropySize = 16 @@ -70,15 +70,15 @@ const ( // will result in. NumMnemonicWords = 24 - // saltSize is the size of the salt we'll generate to use with scrypt + // SaltSize is the size of the salt we'll generate to use with scrypt // to generate a key for use within aez from the user's passphrase. The // role of the salt is to make the creation of rainbow tables // infeasible. - saltSize = 5 + SaltSize = 5 // adSize is the size of the encoded associated data that will be // passed into aez when enciphering and deciphering the seed. The AD - // itself (associated data) is just the CipherSeedVersion and salt. + // itself (associated data) is just the cipher seed version and salt. adSize = 6 // checkSumSize is the size of the checksum applied to the final @@ -93,9 +93,9 @@ const ( // We encode our mnemonic using 24 words, so 264 bits (33 bytes). BitsPerWord = 11 - // saltOffset is the index within an enciphered cipherseed that marks + // saltOffset is the index within an enciphered cipher seed that marks // the start of the salt. - saltOffset = EncipheredCipherSeedSize - checkSumSize - saltSize + saltOffset = EncipheredCipherSeedSize - checkSumSize - SaltSize // checkSumSize is the index within an enciphered cipher seed that // marks the start of the checksum. @@ -103,8 +103,8 @@ const ( ) var ( - // Below at the default scrypt parameters that are tied to - // CipherSeedVersion zero. + // Below at the default scrypt parameters that are tied to cipher seed + // version zero. scryptN = 32768 scryptR = 8 scryptP = 1 @@ -128,8 +128,35 @@ var ( BitcoinGenesisDate = time.Unix(1231006505, 0) ) +// SeedOptions is a type that holds options that configure the generation of a +// new cipher seed. +type SeedOptions struct { + // randomnessSource is the source of randomness that is used to generate + // the salt that is used for encrypting the seed. + randomnessSource io.Reader +} + +// DefaultOptions returns the default seed options. +func DefaultOptions() *SeedOptions { + return &SeedOptions{ + randomnessSource: rand.Reader, + } +} + +// SeedOptionModifier is a function signature for modifying the default +// SeedOptions. +type SeedOptionModifier func(*SeedOptions) + +// WithRandomnessSource returns an option modifier that replaces the default +// randomness source with the given reader. +func WithRandomnessSource(src io.Reader) SeedOptionModifier { + return func(opts *SeedOptions) { + opts.randomnessSource = src + } +} + // CipherSeed is a fully decoded instance of the aezeed scheme. At a high -// level, the encoded cipherseed is the enciphering of: a version byte, a set +// level, the encoded cipher seed is the enciphering of: a version byte, a set // of bytes for a timestamp, the entropy which will be used to directly // construct the HD seed, and finally a checksum over the rest. This scheme was // created as the widely used schemes in the space lack two critical traits: a @@ -151,7 +178,7 @@ var ( // users can encrypt the raw "plaintext" seed under distinct passwords to // produce unique mnemonic phrases. type CipherSeed struct { - // InternalVersion is the version of the plaintext cipherseed. This is + // InternalVersion is the version of the plaintext cipher seed. This is // to be used by wallets to determine if the seed version is compatible // with the derivation schemes they know. InternalVersion uint8 @@ -169,22 +196,27 @@ type CipherSeed struct { // salt is the salt that was used to generate the key from the user's // specified passphrase. - salt [saltSize]byte + salt [SaltSize]byte } // New generates a new CipherSeed instance from an optional source of entropy. // If the entropy isn't provided, then a set of random bytes will be used in -// place. The final argument should be the time at which the seed was created. +// place. The final fixed argument should be the time at which the seed was +// created, followed by optional seed option modifiers. func New(internalVersion uint8, entropy *[EntropySize]byte, - now time.Time) (*CipherSeed, error) { + now time.Time, modifiers ...SeedOptionModifier) (*CipherSeed, error) { - // TODO(roasbeef): pass randomness source? to make fully determinsitc? + opts := DefaultOptions() + for _, modifier := range modifiers { + modifier(opts) + } // If a set of entropy wasn't provided, then we'll read a set of bytes - // from the CSPRNG of our operating platform. + // from the randomness source provided (which by default is the system's + // CSPRNG). var seed [EntropySize]byte if entropy == nil { - if _, err := rand.Read(seed[:]); err != nil { + if _, err := opts.randomnessSource.Read(seed[:]); err != nil { return nil, err } } else { @@ -205,7 +237,7 @@ func New(internalVersion uint8, entropy *[EntropySize]byte, // Next, we'll read a random salt that will be used with scrypt to // eventually derive our key. - if _, err := rand.Read(c.salt[:]); err != nil { + if _, err := opts.randomnessSource.Read(c.salt[:]); err != nil { return nil, err } @@ -252,9 +284,9 @@ func (c *CipherSeed) decode(r io.Reader) error { // encodeAD returns the fully encoded associated data for use when performing // our current enciphering operation. The AD is: version || salt. -func encodeAD(version uint8, salt [saltSize]byte) [adSize]byte { +func encodeAD(version uint8, salt [SaltSize]byte) [adSize]byte { var ad [adSize]byte - ad[0] = byte(version) + ad[0] = version copy(ad[1:], salt[:]) return ad @@ -272,11 +304,13 @@ func extractAD(encipheredSeed [EncipheredCipherSeedSize]byte) [adSize]byte { return ad } -// encipher takes a fully populated cipherseed instance, and enciphers the +// encipher takes a fully populated cipher seed instance, and enciphers the // encoded seed, then appends a randomly generated seed used to stretch the // passphrase out into an appropriate key, then computes a checksum over the // preceding. -func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, error) { +func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, + error) { + var cipherSeedBytes [EncipheredCipherSeedSize]byte // If the passphrase wasn't provided, then we'll use the string @@ -295,7 +329,7 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro return cipherSeedBytes, err } - // Next, we'll encode the serialized plaintext cipherseed into a buffer + // Next, we'll encode the serialized plaintext cipher seed into a buffer // that we'll use for encryption. var seedBytes bytes.Buffer if err := c.encode(&seedBytes); err != nil { @@ -316,7 +350,7 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro // Finally, we'll pack the {version || ciphertext || salt || checksum} // seed into a byte slice for encoding as a mnemonic. - cipherSeedBytes[0] = byte(CipherSeedVersion) + cipherSeedBytes[0] = CipherSeedVersion copy(cipherSeedBytes[1:saltOffset], cipherText) copy(cipherSeedBytes[saltOffset:], c.salt[:]) @@ -335,7 +369,9 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro // cipherTextToMnemonic converts the aez ciphertext appended with the salt to a // 24-word mnemonic pass phrase. -func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic, error) { +func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic, + error) { + var words [NumMnemonicWords]string // First, we'll convert the ciphertext itself into a bitstream for easy @@ -356,7 +392,7 @@ func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic, return words, nil } -// ToMnemonic maps the final enciphered cipher seed to a human readable 24-word +// ToMnemonic maps the final enciphered cipher seed to a human-readable 24-word // mnemonic phrase. The password is optional, as if it isn't specified aezeed // will be used in its place. func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) { @@ -374,7 +410,9 @@ func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) { // Encipher maps the cipher seed to an aez ciphertext using an optional // passphrase. -func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte, error) { +func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte, + error) { + return c.encipher(pass) } @@ -385,7 +423,7 @@ func (c *CipherSeed) BirthdayTime() time.Time { return BitcoinGenesisDate.Add(offset) } -// Mnemonic is a 24-word passphrase as of CipherSeedVersion zero. This +// Mnemonic is a 24-word passphrase as of cipher seed version zero. This // passphrase encodes an encrypted seed triple (version, birthday, entropy). // Additionally, we also encode the salt used with scrypt to derive the key // that the cipher text is encrypted with, and the version which tells us how @@ -426,14 +464,16 @@ func mnemonicToCipherText(mnemonic *Mnemonic) [EncipheredCipherSeedSize]byte { func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) { // First, we'll attempt to decipher the mnemonic by mapping back into // our byte slice and applying our deciphering scheme. - plainSeed, err := m.Decipher(pass) + plainSeed, salt, err := m.Decipher(pass) if err != nil { return nil, err } // If decryption was successful, then we'll decode into a fresh // CipherSeed struct. - var c CipherSeed + c := CipherSeed{ + salt: salt, + } if err := c.decode(bytes.NewReader(plainSeed[:])); err != nil { return nil, err } @@ -445,36 +485,41 @@ func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) { // using the passed passphrase. This function is the opposite of // the encipher method. func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte, - pass []byte) ([DecipheredCipherSeedSize]byte, error) { + pass []byte) ([DecipheredCipherSeedSize]byte, [SaltSize]byte, error) { - var plainSeed [DecipheredCipherSeedSize]byte + var ( + plainSeed [DecipheredCipherSeedSize]byte + salt [SaltSize]byte + ) // Before we do anything, we'll ensure that the version is one that we // understand. Otherwise, we won't be able to decrypt, or even parse // the cipher seed. - if uint8(cipherSeedBytes[0]) != CipherSeedVersion { - return plainSeed, ErrIncorrectVersion + if cipherSeedBytes[0] != CipherSeedVersion { + return plainSeed, salt, ErrIncorrectVersion } // Next, we'll slice off the salt from the pass cipher seed, then // snip off the end of the cipher seed, ignoring the version, and // finally the checksum. - salt := cipherSeedBytes[saltOffset : saltOffset+saltSize] + copy(salt[:], cipherSeedBytes[saltOffset:saltOffset+SaltSize]) cipherSeed := cipherSeedBytes[1:saltOffset] checksum := cipherSeedBytes[checkSumOffset:] // Before we perform any crypto operations, we'll re-create and verify // the checksum to ensure that the user input the proper set of words. - freshChecksum := crc32.Checksum(cipherSeedBytes[:checkSumOffset], crcTable) + freshChecksum := crc32.Checksum( + cipherSeedBytes[:checkSumOffset], crcTable, + ) if freshChecksum != binary.BigEndian.Uint32(checksum) { - return plainSeed, ErrIncorrectMnemonic + return plainSeed, salt, ErrIncorrectMnemonic } // With the salt separated from the cipher text, we'll now obtain the // key used for encryption. - key, err := scrypt.Key(pass, salt, scryptN, scryptR, scryptP, keyLen) + key, err := scrypt.Key(pass, salt[:], scryptN, scryptR, scryptP, keyLen) if err != nil { - return plainSeed, err + return plainSeed, salt, err } // We'll also extract the AD that will be required to properly pass the @@ -488,18 +533,19 @@ func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte, key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil, ) if !ok { - return plainSeed, ErrInvalidPass + return plainSeed, salt, ErrInvalidPass } copy(plainSeed[:], plainSeedBytes) - return plainSeed, nil + return plainSeed, salt, nil } // Decipher attempts to decipher the encoded mnemonic by first mapping to the // original ciphertext, then applying our deciphering scheme. ErrInvalidPass // will be returned if the passphrase is incorrect. -func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error) { +func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, + [SaltSize]byte, error) { // Before we attempt to map the mnemonic back to the original // ciphertext, we'll ensure that all the word are actually a part of @@ -512,10 +558,11 @@ func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error) for i, word := range m { if _, ok := wordDict[word]; !ok { emptySeed := [DecipheredCipherSeedSize]byte{} - return emptySeed, ErrUnknownMnenomicWord{ - Word: word, - Index: uint8(i), - } + return emptySeed, [SaltSize]byte{}, + ErrUnknownMnemonicWord{ + Word: word, + Index: uint8(i), + } } } @@ -537,20 +584,20 @@ func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error) } // ChangePass takes an existing mnemonic, and passphrase for said mnemonic and -// re-enciphers the plaintext cipher seed into a brand new mnemonic. This can +// re-enciphers the plaintext cipher seed into a brand-new mnemonic. This can // be used to allow users to re-encrypt the same seed with multiple pass // phrases, or just change the passphrase on an existing seed. func (m *Mnemonic) ChangePass(oldPass, newPass []byte) (Mnemonic, error) { - var newmnemonic Mnemonic + var newMnemonic Mnemonic // First, we'll try to decrypt the current mnemonic using the existing // passphrase. If this fails, then we can't proceed any further. cipherSeed, err := m.ToCipherSeed(oldPass) if err != nil { - return newmnemonic, err + return newMnemonic, err } - // If the deciperhing was successful, then we'll now re-encipher using + // If the deciphering was successful, then we'll now re-encipher using // the new user provided passphrase. return cipherSeed.ToMnemonic(newPass) } diff --git a/aezeed/cipherseed_test.go b/aezeed/cipherseed_test.go index 8f3043d4a..7547f25e9 100644 --- a/aezeed/cipherseed_test.go +++ b/aezeed/cipherseed_test.go @@ -6,6 +6,8 @@ import ( "testing" "testing/quick" "time" + + "github.com/stretchr/testify/require" ) // TestVector defines the values that are used to create a fully initialized @@ -14,7 +16,7 @@ type TestVector struct { version uint8 time time.Time entropy [EntropySize]byte - salt [saltSize]byte + salt [SaltSize]byte password []byte expectedMnemonic [NumMnemonicWords]string expectedBirthday uint16 @@ -27,56 +29,48 @@ var ( 0x0d, 0xe7, 0x95, 0xe4, 0x1e, 0x0b, 0x4c, 0xfd, } - testSalt = [saltSize]byte{ + 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, + 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", }, - { - 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, + 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[:]) - } + 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 @@ -84,41 +78,60 @@ func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed, 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. + // 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) - } + 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 = testSalt + // 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. + // 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) - } + require.NoError(t, 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) - } + 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. @@ -131,23 +144,17 @@ func TestEmptyPassphraseDerivation(t *testing.T) { // 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) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + require.NoError(t, 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) - } + require.NoError(t, err) // Finally, we'll ensure that the uncovered cipher seed matches // precisely. @@ -165,23 +172,17 @@ func TestManualEntropyGeneration(t *testing.T) { // 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) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + require.NoError(t, 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) - } + require.NoError(t, err) // Finally, we'll ensure that the uncovered cipher seed matches // precisely. @@ -189,7 +190,7 @@ func TestManualEntropyGeneration(t *testing.T) { } // TestInvalidPassphraseRejection tests if a caller attempts to use the -// incorrect passprhase for an enciphered seed, then the proper error is +// incorrect passphrase for an enciphered seed, then the proper error is // returned. func TestInvalidPassphraseRejection(t *testing.T) { t.Parallel() @@ -197,23 +198,18 @@ func TestInvalidPassphraseRejection(t *testing.T) { // 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) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + require.NoError(t, 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) - } + _, err = mnemonic.ToCipherSeed(wrongPass) + require.Equal(t, ErrInvalidPass, err) } // TestRawEncipherDecipher tests that callers are able to use the raw methods @@ -224,36 +220,29 @@ func TestRawEncipherDecipher(t *testing.T) { // 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) - } + require.NoError(t, err) - // With the cipherseed obtained, we'll now use the raw encipher method + // With the cipher seed 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) - } + require.NoError(t, err) mnemonic, err := cipherTextToMnemonic(cipherText) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + 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, err := mnemonic.Decipher(pass) - if err != nil { - t.Fatalf("unable to decipher: %v", err) - } + 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. - var newSeed CipherSeed - err = newSeed.decode(bytes.NewReader(plainSeedBytes[:])) - if err != nil { - t.Fatalf("unable to decode cipher seed: %v", err) + newSeed := CipherSeed{ + salt: salt, } + err = newSeed.decode(bytes.NewReader(plainSeedBytes[:])) + require.NoError(t, err) assertCipherSeedEqual(t, cipherSeed, &newSeed) } @@ -266,17 +255,13 @@ func TestInvalidExternalVersion(t *testing.T) { // 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) - } + require.NoError(t, err) - // With the cipherseed obtained, we'll now use the raw encipher method + // 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) - if err != nil { - t.Fatalf("unable to encipher seed: %v", err) - } + require.NoError(t, err) // Now that we have the cipher text, we'll modify the first byte to be // an invalid version. @@ -284,11 +269,8 @@ func TestInvalidExternalVersion(t *testing.T) { // 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) - } + _, _, err = decipherCipherSeed(cipherText, []byte("kek")) + require.Equal(t, ErrIncorrectVersion, err) } // TestChangePassphrase tests that we're able to generate a cipher seed, then @@ -300,31 +282,23 @@ func TestChangePassphrase(t *testing.T) { // 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) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + 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. + // 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) - } + 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) - if err != nil { - t.Fatalf("unable to decipher cipher seed: %v", err) - } + 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*. @@ -332,7 +306,7 @@ func TestChangePassphrase(t *testing.T) { } // TestChangePassphraseWrongPass tests that if we have a valid enciphered -// cipherseed, but then try to change the password with the *wrong* password, +// 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() @@ -340,27 +314,21 @@ func TestChangePassphraseWrongPass(t *testing.T) { // 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) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + 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 + // 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) - } + require.Equal(t, ErrInvalidPass, err) } // TestMnemonicEncoding uses quickcheck like property based testing to ensure @@ -397,7 +365,7 @@ func TestMnemonicEncoding(t *testing.T) { } // 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 +// 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() @@ -406,7 +374,7 @@ func TestEncipherDecipher(t *testing.T) { // 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. + // recover the original cipher seed. mainScenario := func(version uint8, entropy [EntropySize]byte, nowInt int64, pass [20]byte) bool { @@ -458,15 +426,17 @@ func TestEncipherDecipher(t *testing.T) { // 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 + // 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(now.Sub(BitcoinGenesisDate) / (time.Hour * 24)), + Birthday: uint16(numDaysSinceGenesis), Entropy: entropy, } @@ -506,25 +476,21 @@ func TestSeedEncodeDecode(t *testing.T) { } } -// TestDecipherUnknownMnenomicWord tests that if we obtain a mnemonic, the +// 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 TestDecipherUnknownMnenomicWord(t *testing.T) { +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()) - if err != nil { - t.Fatalf("unable to create seed: %v", err) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + 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. @@ -532,58 +498,36 @@ func TestDecipherUnknownMnenomicWord(t *testing.T) { mnemonic[randIndex] = "kek" // If we attempt to map back to the original cipher seed now, then we - // should get ErrUnknownMnenomicWord. + // should get ErrUnknownMnemonicWord. _, err = mnemonic.ToCipherSeed(pass) - if err == nil { - t.Fatalf("expected ErrUnknownMnenomicWord error") - } + wordErr := &ErrUnknownMnemonicWord{} + require.ErrorAs(t, err, wordErr) + require.Equal(t, "kek", wordErr.Word) + require.Equal(t, uint8(randIndex), wordErr.Index) - 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 + // 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. + // should get ErrUnknownMnemonicWord. _, 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) - } + require.ErrorAs(t, err, wordErr) } -// TestDecipherIncorrectMnemonic tests that if we obtain a cipherseed, but then +// 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()) - if err != nil { - t.Fatalf("unable to create seed: %v", err) - } + 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) - if err != nil { - t.Fatalf("unable to create mnemonic: %v", err) - } + require.NoError(t, err) // We'll now swap out two words from the mnemonic, which should trigger // a checksum failure. @@ -593,11 +537,9 @@ func TestDecipherIncorrectMnemonic(t *testing.T) { // 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. + // should get ErrIncorrectMnemonic. _, err = mnemonic.ToCipherSeed(pass) - if err != ErrIncorrectMnemonic { - t.Fatalf("expected ErrIncorrectMnemonic error") - } + require.Equal(t, ErrIncorrectMnemonic, err) } // TODO(roasbeef): add test failure checksum fail is modified, new error diff --git a/aezeed/errors.go b/aezeed/errors.go index 34af964bf..8f213c069 100644 --- a/aezeed/errors.go +++ b/aezeed/errors.go @@ -18,9 +18,9 @@ var ( "match") ) -// ErrUnknownMnenomicWord is returned when attempting to decipher and +// ErrUnknownMnemonicWord is returned when attempting to decipher and // enciphered mnemonic, but a word encountered isn't a member of our word list. -type ErrUnknownMnenomicWord struct { +type ErrUnknownMnemonicWord struct { // Word is the unknown word in the mnemonic phrase. Word string @@ -29,8 +29,8 @@ type ErrUnknownMnenomicWord struct { Index uint8 } -// Error returns a human readable string describing the error. -func (e ErrUnknownMnenomicWord) Error() string { +// Error returns a human-readable string describing the error. +func (e ErrUnknownMnemonicWord) Error() string { return fmt.Sprintf("word %v isn't a part of default word list "+ "(index=%v)", e.Word, e.Index) } diff --git a/config_builder.go b/config_builder.go index 71ab15611..18e9a3f6f 100644 --- a/config_builder.go +++ b/config_builder.go @@ -965,14 +965,13 @@ func waitForWalletPassword(cfg *Config, // seed. If it's greater than the current key derivation // version, then we'll return an error as we don't understand // this. - const latestVersion = keychain.KeyDerivationVersion if cipherSeed != nil && - cipherSeed.InternalVersion != latestVersion { + !keychain.IsKnownVersion(cipherSeed.InternalVersion) { return nil, fmt.Errorf("invalid internal "+ - "seed version %v, current version is %v", + "seed version %v, current max version is %v", cipherSeed.InternalVersion, - keychain.KeyDerivationVersion) + keychain.CurrentKeyDerivationVersion) } loader, err := btcwallet.NewWalletLoader( diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md index 4a1e49534..c3c824b3a 100644 --- a/docs/release-notes/release-notes-0.15.0.md +++ b/docs/release-notes/release-notes-0.15.0.md @@ -29,6 +29,11 @@ The `walletrpc.SignPsbt` RPC now also supports [Taproot PSBT signing](https://github.com/lightningnetwork/lnd/pull/6450) to fully support remote signing with Taproot outputs. +The internal version of the `aezeed` [was bumped to `1` to mark new seeds that +were created after introducing the Taproot key +derivation](https://github.com/lightningnetwork/lnd/pull/6524) to simplify +detecting Taproot compatibility of a seed. + ## MuSig2 The [`signrpc.Signer` RPC service now supports EXPERIMENTAL MuSig2 diff --git a/keychain/derivation.go b/keychain/derivation.go index 0d6a4ee5b..0e42ae966 100644 --- a/keychain/derivation.go +++ b/keychain/derivation.go @@ -8,11 +8,20 @@ import ( ) const ( - // KeyDerivationVersion is the version of the key derivation schema - // defined below. We use a version as this means that we'll be able to - // accept new seed in the future and be able to discern if the software - // is compatible with the version of the seed. - KeyDerivationVersion = 0 + // KeyDerivationVersionLegacy is the previous version of the key + // derivation schema defined below. We use a version as this means that + // we'll be able to accept new seed in the future and be able to discern + // if the software is compatible with the version of the seed. + KeyDerivationVersionLegacy = 0 + + // KeyDerivationVersionTaproot is the most recent version of the key + // derivation scheme that marks the introduction of the Taproot + // derivation with BIP0086 support. + KeyDerivationVersionTaproot = 1 + + // CurrentKeyDerivationVersion is the current default key derivation + // version that is used for new seeds. + CurrentKeyDerivationVersion = KeyDerivationVersionTaproot // BIP0043Purpose is the "purpose" value that we'll use for the first // version or our key derivation scheme. All keys are expected to be @@ -25,6 +34,13 @@ const ( BIP0043Purpose = 1017 ) +// IsKnownVersion returns true if the given version is one of the known +// derivation scheme versions as defined by this package. +func IsKnownVersion(internalVersion uint8) bool { + return internalVersion == KeyDerivationVersionLegacy || + internalVersion == KeyDerivationVersionTaproot +} + var ( // MaxKeyRangeScan is the maximum number of keys that we'll attempt to // scan with if a caller knows the public key, but not the KeyLocator diff --git a/walletunlocker/service.go b/walletunlocker/service.go index ce3997666..e473a0c5b 100644 --- a/walletunlocker/service.go +++ b/walletunlocker/service.go @@ -309,7 +309,7 @@ func (u *UnlockerService) GenSeed(_ context.Context, // instance. // cipherSeed, err := aezeed.New( - keychain.KeyDerivationVersion, &entropy, time.Now(), + keychain.CurrentKeyDerivationVersion, &entropy, time.Now(), ) if err != nil { return nil, err diff --git a/walletunlocker/service_test.go b/walletunlocker/service_test.go index 88274459b..5c723d759 100644 --- a/walletunlocker/service_test.go +++ b/walletunlocker/service_test.go @@ -91,7 +91,7 @@ func createSeedAndMnemonic(t *testing.T, pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) { cipherSeed, err := aezeed.New( - keychain.KeyDerivationVersion, &testEntropy, time.Now(), + keychain.CurrentKeyDerivationVersion, &testEntropy, time.Now(), ) require.NoError(t, err)