Merge pull request #773 from Roasbeef/aezeed
aezeed: add new package implementing the aezeed cipher seed scheme
This commit is contained in:
7 files changed
+3248
-2
No files matched your search
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+2073
File diff suppressed because it is too large.
Load diff
Generated
+12
-2
@@ -1,6 +1,13 @@
|
|||||||
hash: b7b9aed5daf9b2fdc4083d310ab2cabfc86e08db6796b9301b2e2d73c9bd6174
|
hash: 31c7557ef187f50de28557359e5179a47d5f4f153ec9b4f1ad264f771e7d1b5c
|
||||||
updated: 2018-02-23T15:57:52.662879082-08:00
|
updated: 2018-03-01T16:45:01.924542733-08:00
|
||||||
imports:
|
imports:
|
||||||
|
- name: git.schwanenlied.me/yawning/bsaes.git
|
||||||
|
version: e06297f34865a50b8e473105e52cb64ad1b55da8
|
||||||
|
subpackages:
|
||||||
|
- ct32
|
||||||
|
- ct64
|
||||||
|
- ghash
|
||||||
|
- internal/modes
|
||||||
- name: github.com/aead/chacha20
|
- name: github.com/aead/chacha20
|
||||||
version: d31a916ded42d1640b9d89a26f8abd53cc96790c
|
version: d31a916ded42d1640b9d89a26f8abd53cc96790c
|
||||||
subpackages:
|
subpackages:
|
||||||
@@ -140,9 +147,12 @@ imports:
|
|||||||
version: 501572607d0273fc75b3b261fa4904d63f6ffa0e
|
version: 501572607d0273fc75b3b261fa4904d63f6ffa0e
|
||||||
- name: github.com/urfave/cli
|
- name: github.com/urfave/cli
|
||||||
version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1
|
version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1
|
||||||
|
- name: github.com/Yawning/aez
|
||||||
|
version: 4dad034d9db2caec23fb8f69b9160ae16f8d46a3
|
||||||
- name: golang.org/x/crypto
|
- name: golang.org/x/crypto
|
||||||
version: 49796115aa4b964c318aad4f3084fdb41e9aa067
|
version: 49796115aa4b964c318aad4f3084fdb41e9aa067
|
||||||
subpackages:
|
subpackages:
|
||||||
|
- blake2b
|
||||||
- chacha20poly1305
|
- chacha20poly1305
|
||||||
- curve25519
|
- curve25519
|
||||||
- ed25519
|
- ed25519
|
||||||
|
|||||||
@@ -80,3 +80,7 @@ import:
|
|||||||
- package: github.com/rogpeppe/fastuuid
|
- package: github.com/rogpeppe/fastuuid
|
||||||
- package: gopkg.in/errgo.v1
|
- package: gopkg.in/errgo.v1
|
||||||
- package: github.com/miekg/dns
|
- package: github.com/miekg/dns
|
||||||
|
- package: github.com/Yawning/aez
|
||||||
|
version: 4dad034d9db2caec23fb8f69b9160ae16f8d46a3
|
||||||
|
- package: github.com/kkdai/bstream
|
||||||
|
version: f391b8402d23024e7c0f624b31267a89998fca95
|
||||||
Reference in new issue
Block a user