From ffac0336e6e88b3a7a7198febc077efcf9eb758f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 22 Feb 2018 17:12:41 -0800 Subject: [PATCH 1/4] aezeed: add new package implementing the aezeed cipher seed scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit, we add a new package implementing the aezeed cipher seed scheme. 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. The aezeed scheme addresses these two drawbacks and adds a number of desirable features. First, we start with the following plaintext seed: {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. 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 aezeed, 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 4, which acts as a 32-bit checksum. We’ll encrypt with our generated seed, and use an AD of (version || salt). We'll them compute a checksum over all the data, using crc-32, appending the result to the end. * Finally, we’ll encode this 33-byte cipher text using the default world list of BIP 39 to produce 24 english words. 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 passphrase) 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. * We're able to verify that a user has input the incorrect passphrase, and that the user has input the incorrect mnemonic independently. --- aezeed/cipherseed.go | 547 +++++++++++ aezeed/errors.go | 30 + aezeed/wordlist.go | 2073 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2650 insertions(+) create mode 100644 aezeed/cipherseed.go create mode 100644 aezeed/errors.go create mode 100644 aezeed/wordlist.go diff --git a/aezeed/cipherseed.go b/aezeed/cipherseed.go new file mode 100644 index 00000000..3c34754d --- /dev/null +++ b/aezeed/cipherseed.go @@ -0,0 +1,547 @@ +package aezeed + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "hash/crc32" + "io" + "strings" + "time" + + "github.com/Yawning/aez" + "github.com/kkdai/bstream" + + "golang.org/x/crypto/scrypt" +) + +const ( + // CipherSeedVersion is the current version of the aezeed scheme as + // defined in this package. This version indicates the following + // parameters for the deciphered cipher seed: a 1 byte version, 2 bytes + // for the Bitcoin Days Genesis timestamp, and 16 bytes for entropy. It + // also governs how the cipher seed should be enciphered. In this + // version we take the deciphered seed, create a 5 byte salt, use that + // with an optional passphrase to generate a 32-byte key (via scrypt), + // then encipher with aez (using the salt and version as AD). The final + // enciphered seed is: version || ciphertext || salt. + CipherSeedVersion uint8 = 0 + + // DecipheredCipherSeedSize is the size of the plaintext seed resulting + // from deciphering the cipher seed. The size consists of the + // following: + // + // * 1 byte version || 2 bytes timestamp || 16 bytes of entropy. + // + // The version is used by wallets to know how to re-derive relevant + // addresses, the 2 byte timestamp a BDG (Bitcoin Days Genesis) offset, + // and finally, the 16 bytes to be used to generate the HD wallet seed. + DecipheredCipherSeedSize = 19 + + // EncipheredCipherSeedSize is the size of the fully encoded+enciphered + // cipher seed. We first obtain the enciphered plaintext seed by + // carrying out the enciphering as governed in the current version. We + // then take that enciphered seed (now 19+4=23 bytes due to ciphertext + // expansion, essentially a checksum) and prepend a version, then + // append the salt, and then take a checksum of everything. The + // checksum allows us to verify that the user input the correct set of + // words, then we can verify the passphrase due to the internal MAC + // equiv. The final breakdown is: + // + // * 1 byte version || 23 byte enciphered seed || 5 byte salt || 4 byte checksum + // + // With CipherSeedVersion we encipher as follows: we use + // scrypt(n=32768, r=8, p=1) to derive a 32-byte key from an optional + // user passphrase. We then encipher the plaintext seed using a value + // of tau (with aez) of 8-bytes (so essentially a 32-bit MAC). When + // enciphering, we include the version and scrypt salt as the AD. This + // gives us a total of 33 bytes. These 33 bytes fit cleanly into 24 + // mnemonic words. + EncipheredCipherSeedSize = 33 + + // CipherTextExpansion is the number of bytes that will be added as + // redundancy for the enciphering scheme implemented by aez. This can + // be seen as the size of the equivalent MAC. + CipherTextExpansion = 4 + + // EntropySize is the number of bytes of entropy we'll use the generate + // the seed. + EntropySize = 16 + + // NummnemonicWords is the number of words that an encoded cipher seed + // will result in. + NummnemonicWords = 24 + + // 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 + + // 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. + adSize = 6 + + // checkSumSize is the size of the checksum applied to the final + // encoded ciphertext. + checkSumSize = 4 + + // keyLen is the size of the key that we'll use for encryption with + // aez. + keyLen = 32 + + // bitsPerWord is the number of bits each word in the wordlist encodes. + // We encode our mnemonic using 24 words, so 264 bits (33 bytes). + bitsPerWord = 11 + + // saltOffset is the index within an enciphered cipherseed that marks + // the start of the salt. + saltOffset = EncipheredCipherSeedSize - checkSumSize - saltSize + + // checkSumSize is the index within an enciphered cipher seed that + // marks the start of the checksum. + checkSumOffset = EncipheredCipherSeedSize - checkSumSize + + // encipheredSeedSize is the size of the cipherseed before applying the + // external version, salt, and checksum for the final encoding. + encipheredSeedSize = DecipheredCipherSeedSize + CipherTextExpansion +) + +var ( + // Below at the default scrypt parameters that are tied to + // CipherSeedVersion zero. + scryptN = 32768 + scryptR = 8 + scryptP = 1 + + // crcTable is a table that presents the polynomial we'll use for + // computing our checksum. + crcTable = crc32.MakeTable(crc32.Castagnoli) + + // defaultPassphras is the default passphrase that will be used for + // encryption in the case that the user chooses not to specify their + // own passphrase. + defaultPassphrase = []byte("aezeed") +) + +var ( + // bitcoinGenesisDate is the timestamp of Bitcoin's genesis block. + // We'll use this value in order to create a compact birthday for the + // seed. The birthday will be interested as the number of days since + // the genesis date. We refer to this time period as ABE (after Bitcoin + // era). + bitcoinGenesisDate = time.Unix(1231006505, 0) +) + +// 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 +// 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 +// version byte, and a birthday timestamp. The version allows us to modify the +// details of the scheme in the future, and the birthday gives wallets a limit +// of how far back in the chain they'll need to start scanning. We also add an +// external version to the enciphering plaintext seed. With this addition, +// seeds are able to be "upgraded" (to diff params, or entirely diff crypt), +// while maintaining the semantics of the plaintext seed. +// +// The core of the scheme is the usage of aez to carefully control the size of +// the final encrypted seed. With the current parameters, this scheme can be +// encoded using a 24 word mnemonic. We use 4 bytes of ciphertext expansion +// when enciphering the raw seed, giving us the equivalent of 40-bit MAC (as we +// check for a particular seed version). Using the external 4 byte checksum, +// we're able to ensure that the user input the correct set of words. Finally, +// the password in the scheme is optional. If not specified, "aezeed" will be +// used as the password. Otherwise, the addition of the password means that +// 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 + // to be used by wallets to determine if the seed version is compatible + // with the derivation schemes they know. + InternalVersion uint8 + + // Birthday is the time that the seed was created. This is expressed as + // the number of days since the timestamp in the Bitcoin genesis block. + // We use days as seconds gives us wasted granularity. The oldest seed + // that we can encode using this format is through the date 2188. + Birthday uint16 + + // Entropy is a set of bytes generated via a CSPRNG. This is the value + // that should be used to directly generate the HD root, as defined + // within BIP0032. + Entropy [EntropySize]byte + + // salt is the salt that was used to generate the key from the user's + // specified passphrase. + 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. +func New(internalVersion uint8, entropy *[EntropySize]byte, + now time.Time) (*CipherSeed, error) { + + // TODO(roasbeef): pass randomness source? to make fully determinsitc? + + // If a set of entropy wasn't provided, then we'll read a set of bytes + // from the CSPRNG of our operating platform. + var seed [EntropySize]byte + if entropy == nil { + if _, err := rand.Read(seed[:]); err != nil { + return nil, err + } + } else { + // Otherwise, we'll copy the set of bytes. + copy(seed[:], entropy[:]) + } + + // To compute our "birthday", we'll first use the current time, then + // subtract that from the Bitcoin Genesis Date. We'll then convert that + // value to days. + birthday := uint16(now.Sub(bitcoinGenesisDate) / (time.Hour * 24)) + + c := &CipherSeed{ + InternalVersion: internalVersion, + Birthday: birthday, + Entropy: seed, + } + + // 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 { + return nil, err + } + + return c, nil +} + +// encode attempts to encode the target cipherSeed into the passed io.Writer +// instance. +func (c *CipherSeed) encode(w io.Writer) error { + err := binary.Write(w, binary.BigEndian, c.InternalVersion) + if err != nil { + return err + } + + if err := binary.Write(w, binary.BigEndian, c.Birthday); err != nil { + return err + } + + if _, err := w.Write(c.Entropy[:]); err != nil { + return err + } + + return nil +} + +// decode attempts to decode an encoded cipher seed instance into the target +// CipherSeed struct. +func (c *CipherSeed) decode(r io.Reader) error { + err := binary.Read(r, binary.BigEndian, &c.InternalVersion) + if err != nil { + return err + } + + if err := binary.Read(r, binary.BigEndian, &c.Birthday); err != nil { + return err + } + + if _, err := io.ReadFull(r, c.Entropy[:]); err != nil { + return err + } + + return nil +} + +// 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 { + var ad [adSize]byte + ad[0] = byte(version) + copy(ad[1:], salt[:]) + + return ad +} + +// extractAD extracts an associated data from a fully encoded and enciphered +// cipher seed. This is to be used when attempting to decrypt an enciphered +// cipher seed. +func extractAD(encipheredSeed [EncipheredCipherSeedSize]byte) [adSize]byte { + var ad [adSize]byte + ad[0] = encipheredSeed[0] + + copy(ad[1:], encipheredSeed[saltOffset:checkSumOffset]) + + return ad +} + +// encipher takes a fully populated cipherseed 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) { + var cipherSeedBytes [EncipheredCipherSeedSize]byte + + // If the passphrase wasn't provided, then we'll use the string + // "aezeed" in place. + passphrase := pass + if len(passphrase) == 0 { + passphrase = defaultPassphrase + } + + // With our salt pre-generated, we'll now run the password through a + // KDF to obtain the key we'll use for encryption. + key, err := scrypt.Key( + passphrase, c.salt[:], scryptN, scryptR, scryptP, keyLen, + ) + if err != nil { + return cipherSeedBytes, err + } + + // Next, we'll encode the serialized plaintext cipherseed into a buffer + // that we'll use for encryption. + var seedBytes bytes.Buffer + if err := c.encode(&seedBytes); err != nil { + return cipherSeedBytes, err + } + + // With our plaintext seed encoded, we'll now construct the AD that + // will be passed to the encryption operation. This ensures to + // authenticate both the salt and the external version. + ad := encodeAD(CipherSeedVersion, c.salt) + + // With all items assembled, we'll now encipher the plaintext seed + // with our AD, key, and MAC size. + cipherSeed := seedBytes.Bytes() + cipherText := aez.Encrypt( + key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil, + ) + + // Finally, we'll pack the {version || ciphertext || salt || checksum} + // seed into a byte slice for encoding as a mnemonic. + cipherSeedBytes[0] = byte(CipherSeedVersion) + copy(cipherSeedBytes[1:saltOffset], cipherText) + copy(cipherSeedBytes[saltOffset:], c.salt[:]) + + // With the seed mostly assembled, we'll now compute a checksum all the + // contents. + checkSum := crc32.Checksum(cipherSeedBytes[:checkSumOffset], crcTable) + + // With our checksum computed, we can finish encoding the full cipher + // seed. + var checkSumBytes [4]byte + binary.BigEndian.PutUint32(checkSumBytes[:], checkSum) + copy(cipherSeedBytes[checkSumOffset:], checkSumBytes[:]) + + return cipherSeedBytes, nil +} + +// cipherTextToMnemonic converts the aez ciphertext appended with the salt to a +// 24-word mnemonic pass phrase. +func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic, error) { + var words [NummnemonicWords]string + + // First, we'll convert the ciphertext itself into a bitstream for easy + // manipulation. + cipherBits := bstream.NewBStreamReader(cipherText[:]) + + // With our bitstream obtained, we'll read 11 bits at a time, then use + // that to index into our word list to obtain the next word. + for i := 0; i < NummnemonicWords; i++ { + index, err := cipherBits.ReadBits(bitsPerWord) + if err != nil { + return words, nil + } + + words[i] = defaultWordList[index] + } + + return words, nil +} + +// 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) { + // First, we'll convert the valid seed triple into an aez cipher text + // with our KDF salt appended to it. + cipherText, err := c.encipher(pass) + if err != nil { + return Mnemonic{}, nil + } + + // Now that we have our cipher text, we'll convert it into a mnemonic + // phrase. + return cipherTextToMnemonic(cipherText) +} + +// Encipher maps the cipher seed to an aez ciphertext using an optional +// passphrase. +func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte, error) { + return c.encipher(pass) +} + +// Mnemonic is a 24-word passphrase as of CipherSeedVersion 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 +// to decipher the seed. +type Mnemonic [NummnemonicWords]string + +// mnemonicToCipherText converts a 24-word mnemonic phrase into a 33 byte +// cipher text. +// +// NOTE: This assumes that all words have already been checked to be amongst +// our word list. +func mnemonicToCipherText(mnemonic *Mnemonic) [EncipheredCipherSeedSize]byte { + var cipherText [EncipheredCipherSeedSize]byte + + // We'll now perform the reverse mapping to that of + // cipherTextToMnemonic: we'll get the index of the word, then write + // out that index to the bit stream. + cipherBits := bstream.NewBStreamWriter(EncipheredCipherSeedSize) + for _, word := range mnemonic { + // Using the reverse word map, we'll locate the index of this + // word within the word list. + index := uint64(reverseWordMap[word]) + + // With the index located, we'll now write this out to the + // bitstream, appending to what's already there. + cipherBits.WriteBits(index, bitsPerWord) + } + + copy(cipherText[:], cipherBits.Bytes()) + + return cipherText +} + +// ToCipherSeed attempts to map the mnemonic to the original cipher text byte +// slice. Then we'll attempt to decrypt the ciphertext using aez with the +// passed passphrase, using the last 5 bytes of the ciphertext as a salt for +// the KDF. +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) + if err != nil { + return nil, err + } + + // If decryption was successful, then we'll decode into a fresh + // CipherSeed struct. + var c CipherSeed + if err := c.decode(bytes.NewReader(plainSeed[:])); err != nil { + return nil, err + } + + return &c, nil +} + +// decipherCipherSeed attempts to decipher the passed cipher seed ciphertext +// using the passed passphrase. This function is the opposite of +// the encipher method. +func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte, + pass []byte) ([DecipheredCipherSeedSize]byte, error) { + + var plainSeed [DecipheredCipherSeedSize]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 + } + + // 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] + 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) + if freshChecksum != binary.BigEndian.Uint32(checksum) { + return plainSeed, 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) + if err != nil { + return plainSeed, err + } + + // We'll also extract the AD that will be required to properly pass the + // MAC check. + ad := extractAD(cipherSeedBytes) + + // With the key, we'll attempt to decrypt the plaintext. If the + // ciphertext was altered, or the passphrase is incorrect, then we'll + // error out. + plainSeedBytes, ok := aez.Decrypt( + key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil, + ) + if !ok { + return plainSeed, ErrInvalidPass + } + copy(plainSeed[:], plainSeedBytes) + + return plainSeed, nil + +} + +// Decipher attempts to decipher the encoded mnemonic by first mapping to the +// original chipertext, then applying our deciphering scheme. ErrInvalidPass +// will be returned if the passphrase is incorrect. +func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]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 + // the current default word list. + for _, word := range m { + if !strings.Contains(englishWordList, word) { + emptySeed := [DecipheredCipherSeedSize]byte{} + return emptySeed, ErrUnknownMnenomicWord{word} + } + } + + // If the passphrase wasn't provided, then we'll use the string + // "aezeed" in place. + passphrase := pass + if len(passphrase) == 0 { + passphrase = defaultPassphrase + } + + // Next, we'll map the mnemonic phrase back into the original cipher + // text. + cipherText := mnemonicToCipherText(m) + + // Finally, we'll attempt to decipher the enciphered seed. The result + // will be the raw seed minus the ciphertext expansion, external + // version, and salt. + return decipherCipherSeed(cipherText, passphrase) +} + +// ChangePass takes an existing mnemonic, and passphrase for said mnemonic and +// 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 + + // 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 + } + + // If the deciperhing was successful, then we'll now re-encipher using + // the new user provided passphrase. + return cipherSeed.ToMnemonic(newPass) +} diff --git a/aezeed/errors.go b/aezeed/errors.go new file mode 100644 index 00000000..9b1574ac --- /dev/null +++ b/aezeed/errors.go @@ -0,0 +1,30 @@ +package aezeed + +import "fmt" + +var ( + // ErrIncorrectVersion is returned if a seed bares a mismatched + // external version to that of the package executing the aezeed scheme. + ErrIncorrectVersion = fmt.Errorf("wrong seed version") + + // ErrInvalidPass is returned if the user enters an invalid passphrase + // for a particular enciphered mnemonic. + ErrInvalidPass = fmt.Errorf("invalid passphrase") + + // ErrIncorrectMnemonic is returned if we detect that the checksum of + // the specified mnemonic doesn't match. This indicates the user input + // the wrong mnemonic. + ErrIncorrectMnemonic = fmt.Errorf("mnemonic phrase checksum doesn't" + + "match") +) + +// ErrUnknownMnenomicWord is returned when attempting to decipher and +// enciphered mnemonic, but a word encountered isn't a member of our word list. +type ErrUnknownMnenomicWord struct { + word string +} + +// Error returns a human readable string describing the error. +func (e ErrUnknownMnenomicWord) Error() string { + return fmt.Sprintf("word %v isn't a part of default word list", e.word) +} diff --git a/aezeed/wordlist.go b/aezeed/wordlist.go new file mode 100644 index 00000000..09b53e55 --- /dev/null +++ b/aezeed/wordlist.go @@ -0,0 +1,2073 @@ +package aezeed + +import ( + "strings" +) + +var ( + // reverseWordMap maps a word to its position within the default word list. + reverseWordMap map[string]int +) + +func init() { + reverseWordMap = make(map[string]int) + for i, v := range defaultWordList { + reverseWordMap[v] = i + } +} + +// defaultWordList is a slice of the current default word list that's used to +// encode the enciphered seed into a human readable set of words. +var defaultWordList = strings.Split(englishWordList, "\n") + +// englishWordList is an English wordlist that's used as part of version 0 of +// the cipherseed scheme. This is the *same* word list that's recommend for use +// with BIP0039. +var englishWordList = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo` From eb3b5196e1c4a17e684e07b993e7730e1504259a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 22 Feb 2018 17:13:17 -0800 Subject: [PATCH 2/4] aezeed: add comprehensive, mostly property-based tests --- aezeed/cipherseeed_test.go | 513 +++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 aezeed/cipherseeed_test.go 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 +} From 120cebef772c2e78609099e73be08c73a25bfaeb Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 22 Feb 2018 17:14:45 -0800 Subject: [PATCH 3/4] aezeed: add a set of benchmarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit we add a set of benchmarks to be able to measure the enciphering and deciphering speed of the current scheme with the current scrypt parameters. On my laptop I get about 100ms per attempt: ⛰ go test -run=XXX -bench=. goos: darwin goarch: amd64 pkg: github.com/lightningnetwork/lnd/aezeed BenchmarkToMnenonic-4 10 102287840 ns/op BenchmarkFromMnenonic-4 10 105874973 ns/op PASS ok github.com/lightningnetwork/lnd/aezeed 3.036s --- aezeed/bench_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 aezeed/bench_test.go diff --git a/aezeed/bench_test.go b/aezeed/bench_test.go new file mode 100644 index 00000000..c54c1794 --- /dev/null +++ b/aezeed/bench_test.go @@ -0,0 +1,69 @@ +package aezeed + +import ( + "testing" + "time" +) + +var ( + mnemonic Mnemonic + + seed *CipherSeed +) + +// BenchmarkFrommnemonic benchmarks the process of converting a cipher seed +// (given the salt), to an enciphered mnemonic. +func BenchmarkTomnemonic(b *testing.B) { + scryptN = 32768 + scryptR = 8 + scryptP = 1 + + pass := []byte("1234567890abcedfgh") + cipherSeed, err := New(0, nil, time.Now()) + if err != nil { + b.Fatalf("unable to create seed: %v", err) + } + + var r Mnemonic + for i := 0; i < b.N; i++ { + r, err = cipherSeed.ToMnemonic(pass) + if err != nil { + b.Fatalf("unable to encipher: %v", err) + } + } + + b.ReportAllocs() + + mnemonic = r +} + +// BenchmarkToCipherSeed benchmarks the process of deciphering an existing +// enciphered mnemonic. +func BenchmarkToCipherSeed(b *testing.B) { + scryptN = 32768 + scryptR = 8 + scryptP = 1 + + pass := []byte("1234567890abcedfgh") + cipherSeed, err := New(0, nil, time.Now()) + if err != nil { + b.Fatalf("unable to create seed: %v", err) + } + + mnemonic, err := cipherSeed.ToMnemonic(pass) + if err != nil { + b.Fatalf("unable to create mnemonic: %v", err) + } + + var s *CipherSeed + for i := 0; i < b.N; i++ { + s, err = mnemonic.ToCipherSeed(pass) + if err != nil { + b.Fatalf("unable to decipher: %v", err) + } + } + + b.ReportAllocs() + + seed = s +} From d65fe83a592a6ac354cf561d3291b9197c984fba Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 22 Feb 2018 17:15:06 -0800 Subject: [PATCH 4/4] build: add aez and bstream to glide --- glide.lock | 14 ++++++++++++-- glide.yaml | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/glide.lock b/glide.lock index 446f9301..fb4dbd52 100644 --- a/glide.lock +++ b/glide.lock @@ -1,6 +1,13 @@ -hash: b7b9aed5daf9b2fdc4083d310ab2cabfc86e08db6796b9301b2e2d73c9bd6174 -updated: 2018-02-23T15:57:52.662879082-08:00 +hash: 31c7557ef187f50de28557359e5179a47d5f4f153ec9b4f1ad264f771e7d1b5c +updated: 2018-03-01T16:45:01.924542733-08:00 imports: +- name: git.schwanenlied.me/yawning/bsaes.git + version: e06297f34865a50b8e473105e52cb64ad1b55da8 + subpackages: + - ct32 + - ct64 + - ghash + - internal/modes - name: github.com/aead/chacha20 version: d31a916ded42d1640b9d89a26f8abd53cc96790c subpackages: @@ -140,9 +147,12 @@ imports: version: 501572607d0273fc75b3b261fa4904d63f6ffa0e - name: github.com/urfave/cli version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1 +- name: github.com/Yawning/aez + version: 4dad034d9db2caec23fb8f69b9160ae16f8d46a3 - name: golang.org/x/crypto version: 49796115aa4b964c318aad4f3084fdb41e9aa067 subpackages: + - blake2b - chacha20poly1305 - curve25519 - ed25519 diff --git a/glide.yaml b/glide.yaml index 6e6fe6e3..a8b87cd7 100644 --- a/glide.yaml +++ b/glide.yaml @@ -80,3 +80,7 @@ import: - package: github.com/rogpeppe/fastuuid - package: gopkg.in/errgo.v1 - package: github.com/miekg/dns +- package: github.com/Yawning/aez + version: 4dad034d9db2caec23fb8f69b9160ae16f8d46a3 +- package: github.com/kkdai/bstream + version: f391b8402d23024e7c0f624b31267a89998fca95