From 0c7451c97c82a0ef83c06cb2c03034f783eea6b8 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 23 Mar 2018 11:43:19 +0100 Subject: [PATCH 1/2] aezeed: add test vectors and test birthday calculation --- ...cipherseeed_test.go => cipherseed_test.go} | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) rename aezeed/{cipherseeed_test.go => cipherseed_test.go} (85%) diff --git a/aezeed/cipherseeed_test.go b/aezeed/cipherseed_test.go similarity index 85% rename from aezeed/cipherseeed_test.go rename to aezeed/cipherseed_test.go index a62150594..7703bdc54 100644 --- a/aezeed/cipherseeed_test.go +++ b/aezeed/cipherseed_test.go @@ -8,6 +8,18 @@ import ( "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, @@ -15,6 +27,39 @@ var ( 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, @@ -34,10 +79,44 @@ func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed, } } +// TestAezeedVersion0TestVectors tests some fixed test vector values against +// the expected mnemonic words. func TestAezeedVersion0TestVectors(t *testing.T) { t.Parallel() - // TODO(roasbeef): + // 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 From 16ad6aed09cf86155a0db156321659cc97c8957c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 23 Mar 2018 12:17:50 +0100 Subject: [PATCH 2/2] aezeed: add README with the text of the PR --- aezeed/README.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 aezeed/README.md diff --git a/aezeed/README.md b/aezeed/README.md new file mode 100644 index 000000000..be910f236 --- /dev/null +++ b/aezeed/README.md @@ -0,0 +1,103 @@ +# aezeed + +[https://github.com/lightningnetwork/lnd/pull/773](In this PR), +we add a new package implementing the aezeed cipher +seed scheme (based on [http://web.cs.ucdavis.edu/~rogaway/aez/](aez) ). + +This is a new scheme developed that aims to overcome the +two major short comings of BIP39: a lack of a version, and a lack of a +wallet birthday. A lack a version means that wallets may not +necessarily know how to re-derive addresses during the recovery +process. A lack of a birthday means that wallets don’t know how far +back to look in the chain to ensure that they derive all the proper +user addresses. Additionally, BIP39 use a very weak KDF. We use +scrypt with modern parameters (n=32768, r=8, p=1). A set of benchmarks has +been added, on my laptop I get about 100ms per attempt): + +```bash +⛰ go test -run=XXX -bench=. + +goos: linux +goarch: amd64 +pkg: github.com/lightningnetwork/lnd/aezeed +BenchmarkTomnemonic-4 20 93280730 ns/op 33559670 B/op 36 allocs/op +BenchmarkToCipherSeed-4 10 102323892 ns/op 36915684 B/op 41 allocs/op +PASS +ok github.com/lightningnetwork/lnd/aezeed 4.168s +``` + +Aside from addressing the shortcomings of BIP 39 a cipher seed +can: be upgraded, and have it's password changed, + +Sample seed: + +```text +ability dance scatter raw fly dentist bar nominee exhaust wine snap super cost case coconut ticket spread funny grain chimney aspect business quiz ginger +``` + +## Plaintext aezeed encoding + +The aezeed scheme addresses these two drawbacks and adds a number of +desirable features. First, we start with the following plaintext seed: + +```text +1 byte internal version || 2 byte timestamp || 16 bytes of entropy +``` + +The version field is for wallets to be able to know how to re-derive +the keys of the wallet. + +The 2 byte timestamp is expressed in Bitcoin Days Genesis, meaning that +the number of days since the timestamp in Bitcoin’s genesis block. This +allow us to save space, and also avoid using a wasteful level of +granularity. With the currently, this can express time up until 2188. + +Finally, the entropy is raw entropy that should be used to derive +wallet’s HD root. + +## aezeed enciphering/deciperhing + +Next, we’ll take the plaintext seed described above and encipher it to +procure a final cipher text. We’ll then take this cipher text (the +CipherSeed) and encode that using a 24-word mnemonic. The enciphering +process takes a user defined passphrase. If no passphrase is provided, +then the string “aezeed” will be used. + +To encipher a plaintext seed (19 bytes) to arrive at an enciphered +cipher seed (33 bytes), we apply the following operations: + +* First we take the external version an append it to our buffer. The +external version describes how we encipher. For the first version +(version 0), we’ll use scrypt(n=32768, r=8, p=1) and aezeed. +* Next, we’ll use scrypt (with the version 9 params) to generate a +strong key for encryption. We’ll generate a 32-byte key using 5 bytes +as a salt. The usage of the salt is meant to make the creation of +rainbow tables infeasible. +* Next, the enciphering process. We use aez, modern AEAD with +nonce-misuse resistance properties. The important trait we exploit is +that it’s an arbitrary input length block cipher. Additionally, it +has what’s essentially a configurable MAC size. In our scheme we’ll use +a value of 8, which acts as a 64-bit checksum. We’ll encrypt with our +generated seed, and use an AD of (version || salt). +* Finally, we’ll encode this 33-byte cipher text using the default +world list of BIP 39 to produce 24 english words. + +## Properties of the aezeed cipher seed + +The aezeed cipher seed scheme has a few cool properties, notably: + +* The mnemonic itself is a cipher text, meaning leaving it in +plaintext is advisable if the user also set a passphrase. This is in +contrast to BIP 39 where the mnemonic alone (without a passrphase) may +be sufficient to steal funds. +* A cipherseed can be modified to change the passphrase. This +means that if the users wants a stronger passphrase, they can decipher +(with the old passphrase), then encipher (with a new passphrase). +Compared to BIP 39, where if the users used a passphrase, since the +mapping is one way, they can’t change the passphrase of their existing +HD key chain. +* A cipher seed can be upgraded. Since we have an external version, +offline tools can be provided to decipher using the old params, and +encipher using the new params. In the future if we change ciphers, +change scrypt, or just the parameters of scrypt, then users can easily +upgrade their seed with an offline tool. \ No newline at end of file