mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 10:39:01 +01:00
Merge pull request #929 from guggero/aezeed-test
aezeed: add tests and README
This commit is contained in:
commit
28737474ce
103
aezeed/README.md
Normal file
103
aezeed/README.md
Normal 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 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.
|
@ -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
|
Loading…
Reference in New Issue
Block a user