Merge pull request #929 from guggero/aezeed-test

aezeed: add tests and README
This commit is contained in:
Olaoluwa Osuntokun 2018-04-05 19:52:00 -07:00 committed by GitHub
commit 28737474ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 183 additions and 1 deletions

103
aezeed/README.md Normal file
View File

@ -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 dont 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 Bitcoins 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
wallets HD root.
## aezeed enciphering/deciperhing
Next, well take the plaintext seed described above and encipher it to
procure a final cipher text. Well 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), well use scrypt(n=32768, r=8, p=1) and aezeed.
* Next, well use scrypt (with the version 9 params) to generate a
strong key for encryption. Well 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 its an arbitrary input length block cipher. Additionally, it
has whats essentially a configurable MAC size. In our scheme well use
a value of 8, which acts as a 64-bit checksum. Well encrypt with our
generated seed, and use an AD of (version || salt).
* Finally, well 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 cant 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.

View File

@ -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