Merge pull request #6524 from guggero/aezeed-bump
aezeed+keychain: bump internal version of seed to 1
This commit is contained in:
8 files changed
+272
-263
No files matched your search
+96
-49
@@ -62,7 +62,7 @@ const (
|
|||||||
// be seen as the size of the equivalent MAC.
|
// be seen as the size of the equivalent MAC.
|
||||||
CipherTextExpansion = 4
|
CipherTextExpansion = 4
|
||||||
|
|
||||||
// EntropySize is the number of bytes of entropy we'll use the generate
|
// EntropySize is the number of bytes of entropy we'll use to generate
|
||||||
// the seed.
|
// the seed.
|
||||||
EntropySize = 16
|
EntropySize = 16
|
||||||
|
|
||||||
@@ -70,15 +70,15 @@ const (
|
|||||||
// will result in.
|
// will result in.
|
||||||
NumMnemonicWords = 24
|
NumMnemonicWords = 24
|
||||||
|
|
||||||
// saltSize is the size of the salt we'll generate to use with scrypt
|
// 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
|
// 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
|
// role of the salt is to make the creation of rainbow tables
|
||||||
// infeasible.
|
// infeasible.
|
||||||
saltSize = 5
|
SaltSize = 5
|
||||||
|
|
||||||
// adSize is the size of the encoded associated data that will be
|
// adSize is the size of the encoded associated data that will be
|
||||||
// passed into aez when enciphering and deciphering the seed. The AD
|
// passed into aez when enciphering and deciphering the seed. The AD
|
||||||
// itself (associated data) is just the CipherSeedVersion and salt.
|
// itself (associated data) is just the cipher seed version and salt.
|
||||||
adSize = 6
|
adSize = 6
|
||||||
|
|
||||||
// checkSumSize is the size of the checksum applied to the final
|
// checkSumSize is the size of the checksum applied to the final
|
||||||
@@ -93,9 +93,9 @@ const (
|
|||||||
// We encode our mnemonic using 24 words, so 264 bits (33 bytes).
|
// We encode our mnemonic using 24 words, so 264 bits (33 bytes).
|
||||||
BitsPerWord = 11
|
BitsPerWord = 11
|
||||||
|
|
||||||
// saltOffset is the index within an enciphered cipherseed that marks
|
// saltOffset is the index within an enciphered cipher seed that marks
|
||||||
// the start of the salt.
|
// the start of the salt.
|
||||||
saltOffset = EncipheredCipherSeedSize - checkSumSize - saltSize
|
saltOffset = EncipheredCipherSeedSize - checkSumSize - SaltSize
|
||||||
|
|
||||||
// checkSumSize is the index within an enciphered cipher seed that
|
// checkSumSize is the index within an enciphered cipher seed that
|
||||||
// marks the start of the checksum.
|
// marks the start of the checksum.
|
||||||
@@ -103,8 +103,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Below at the default scrypt parameters that are tied to
|
// Below at the default scrypt parameters that are tied to cipher seed
|
||||||
// CipherSeedVersion zero.
|
// version zero.
|
||||||
scryptN = 32768
|
scryptN = 32768
|
||||||
scryptR = 8
|
scryptR = 8
|
||||||
scryptP = 1
|
scryptP = 1
|
||||||
@@ -128,8 +128,35 @@ var (
|
|||||||
BitcoinGenesisDate = time.Unix(1231006505, 0)
|
BitcoinGenesisDate = time.Unix(1231006505, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SeedOptions is a type that holds options that configure the generation of a
|
||||||
|
// new cipher seed.
|
||||||
|
type SeedOptions struct {
|
||||||
|
// randomnessSource is the source of randomness that is used to generate
|
||||||
|
// the salt that is used for encrypting the seed.
|
||||||
|
randomnessSource io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns the default seed options.
|
||||||
|
func DefaultOptions() *SeedOptions {
|
||||||
|
return &SeedOptions{
|
||||||
|
randomnessSource: rand.Reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedOptionModifier is a function signature for modifying the default
|
||||||
|
// SeedOptions.
|
||||||
|
type SeedOptionModifier func(*SeedOptions)
|
||||||
|
|
||||||
|
// WithRandomnessSource returns an option modifier that replaces the default
|
||||||
|
// randomness source with the given reader.
|
||||||
|
func WithRandomnessSource(src io.Reader) SeedOptionModifier {
|
||||||
|
return func(opts *SeedOptions) {
|
||||||
|
opts.randomnessSource = src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CipherSeed is a fully decoded instance of the aezeed scheme. At a high
|
// 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
|
// level, the encoded cipher seed is the enciphering of: a version byte, a set
|
||||||
// of bytes for a timestamp, the entropy which will be used to directly
|
// 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
|
// 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
|
// created as the widely used schemes in the space lack two critical traits: a
|
||||||
@@ -151,7 +178,7 @@ var (
|
|||||||
// users can encrypt the raw "plaintext" seed under distinct passwords to
|
// users can encrypt the raw "plaintext" seed under distinct passwords to
|
||||||
// produce unique mnemonic phrases.
|
// produce unique mnemonic phrases.
|
||||||
type CipherSeed struct {
|
type CipherSeed struct {
|
||||||
// InternalVersion is the version of the plaintext cipherseed. This is
|
// InternalVersion is the version of the plaintext cipher seed. This is
|
||||||
// to be used by wallets to determine if the seed version is compatible
|
// to be used by wallets to determine if the seed version is compatible
|
||||||
// with the derivation schemes they know.
|
// with the derivation schemes they know.
|
||||||
InternalVersion uint8
|
InternalVersion uint8
|
||||||
@@ -169,22 +196,27 @@ type CipherSeed struct {
|
|||||||
|
|
||||||
// salt is the salt that was used to generate the key from the user's
|
// salt is the salt that was used to generate the key from the user's
|
||||||
// specified passphrase.
|
// specified passphrase.
|
||||||
salt [saltSize]byte
|
salt [SaltSize]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// New generates a new CipherSeed instance from an optional source of entropy.
|
// 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
|
// 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.
|
// place. The final fixed argument should be the time at which the seed was
|
||||||
|
// created, followed by optional seed option modifiers.
|
||||||
func New(internalVersion uint8, entropy *[EntropySize]byte,
|
func New(internalVersion uint8, entropy *[EntropySize]byte,
|
||||||
now time.Time) (*CipherSeed, error) {
|
now time.Time, modifiers ...SeedOptionModifier) (*CipherSeed, error) {
|
||||||
|
|
||||||
// TODO(roasbeef): pass randomness source? to make fully determinsitc?
|
opts := DefaultOptions()
|
||||||
|
for _, modifier := range modifiers {
|
||||||
|
modifier(opts)
|
||||||
|
}
|
||||||
|
|
||||||
// If a set of entropy wasn't provided, then we'll read a set of bytes
|
// If a set of entropy wasn't provided, then we'll read a set of bytes
|
||||||
// from the CSPRNG of our operating platform.
|
// from the randomness source provided (which by default is the system's
|
||||||
|
// CSPRNG).
|
||||||
var seed [EntropySize]byte
|
var seed [EntropySize]byte
|
||||||
if entropy == nil {
|
if entropy == nil {
|
||||||
if _, err := rand.Read(seed[:]); err != nil {
|
if _, err := opts.randomnessSource.Read(seed[:]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -205,7 +237,7 @@ func New(internalVersion uint8, entropy *[EntropySize]byte,
|
|||||||
|
|
||||||
// Next, we'll read a random salt that will be used with scrypt to
|
// Next, we'll read a random salt that will be used with scrypt to
|
||||||
// eventually derive our key.
|
// eventually derive our key.
|
||||||
if _, err := rand.Read(c.salt[:]); err != nil {
|
if _, err := opts.randomnessSource.Read(c.salt[:]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +284,9 @@ func (c *CipherSeed) decode(r io.Reader) error {
|
|||||||
|
|
||||||
// encodeAD returns the fully encoded associated data for use when performing
|
// encodeAD returns the fully encoded associated data for use when performing
|
||||||
// our current enciphering operation. The AD is: version || salt.
|
// our current enciphering operation. The AD is: version || salt.
|
||||||
func encodeAD(version uint8, salt [saltSize]byte) [adSize]byte {
|
func encodeAD(version uint8, salt [SaltSize]byte) [adSize]byte {
|
||||||
var ad [adSize]byte
|
var ad [adSize]byte
|
||||||
ad[0] = byte(version)
|
ad[0] = version
|
||||||
copy(ad[1:], salt[:])
|
copy(ad[1:], salt[:])
|
||||||
|
|
||||||
return ad
|
return ad
|
||||||
@@ -272,11 +304,13 @@ func extractAD(encipheredSeed [EncipheredCipherSeedSize]byte) [adSize]byte {
|
|||||||
return ad
|
return ad
|
||||||
}
|
}
|
||||||
|
|
||||||
// encipher takes a fully populated cipherseed instance, and enciphers the
|
// encipher takes a fully populated cipher seed instance, and enciphers the
|
||||||
// encoded seed, then appends a randomly generated seed used to stretch 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
|
// passphrase out into an appropriate key, then computes a checksum over the
|
||||||
// preceding.
|
// preceding.
|
||||||
func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, error) {
|
func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte,
|
||||||
|
error) {
|
||||||
|
|
||||||
var cipherSeedBytes [EncipheredCipherSeedSize]byte
|
var cipherSeedBytes [EncipheredCipherSeedSize]byte
|
||||||
|
|
||||||
// If the passphrase wasn't provided, then we'll use the string
|
// If the passphrase wasn't provided, then we'll use the string
|
||||||
@@ -295,7 +329,7 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro
|
|||||||
return cipherSeedBytes, err
|
return cipherSeedBytes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, we'll encode the serialized plaintext cipherseed into a buffer
|
// Next, we'll encode the serialized plaintext cipher seed into a buffer
|
||||||
// that we'll use for encryption.
|
// that we'll use for encryption.
|
||||||
var seedBytes bytes.Buffer
|
var seedBytes bytes.Buffer
|
||||||
if err := c.encode(&seedBytes); err != nil {
|
if err := c.encode(&seedBytes); err != nil {
|
||||||
@@ -316,7 +350,7 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro
|
|||||||
|
|
||||||
// Finally, we'll pack the {version || ciphertext || salt || checksum}
|
// Finally, we'll pack the {version || ciphertext || salt || checksum}
|
||||||
// seed into a byte slice for encoding as a mnemonic.
|
// seed into a byte slice for encoding as a mnemonic.
|
||||||
cipherSeedBytes[0] = byte(CipherSeedVersion)
|
cipherSeedBytes[0] = CipherSeedVersion
|
||||||
copy(cipherSeedBytes[1:saltOffset], cipherText)
|
copy(cipherSeedBytes[1:saltOffset], cipherText)
|
||||||
copy(cipherSeedBytes[saltOffset:], c.salt[:])
|
copy(cipherSeedBytes[saltOffset:], c.salt[:])
|
||||||
|
|
||||||
@@ -335,7 +369,9 @@ func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte, erro
|
|||||||
|
|
||||||
// cipherTextToMnemonic converts the aez ciphertext appended with the salt to a
|
// cipherTextToMnemonic converts the aez ciphertext appended with the salt to a
|
||||||
// 24-word mnemonic pass phrase.
|
// 24-word mnemonic pass phrase.
|
||||||
func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic, error) {
|
func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic,
|
||||||
|
error) {
|
||||||
|
|
||||||
var words [NumMnemonicWords]string
|
var words [NumMnemonicWords]string
|
||||||
|
|
||||||
// First, we'll convert the ciphertext itself into a bitstream for easy
|
// First, we'll convert the ciphertext itself into a bitstream for easy
|
||||||
@@ -356,7 +392,7 @@ func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic,
|
|||||||
return words, nil
|
return words, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMnemonic maps the final enciphered cipher seed to a human readable 24-word
|
// 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
|
// mnemonic phrase. The password is optional, as if it isn't specified aezeed
|
||||||
// will be used in its place.
|
// will be used in its place.
|
||||||
func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) {
|
func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) {
|
||||||
@@ -374,7 +410,9 @@ func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) {
|
|||||||
|
|
||||||
// Encipher maps the cipher seed to an aez ciphertext using an optional
|
// Encipher maps the cipher seed to an aez ciphertext using an optional
|
||||||
// passphrase.
|
// passphrase.
|
||||||
func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte, error) {
|
func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte,
|
||||||
|
error) {
|
||||||
|
|
||||||
return c.encipher(pass)
|
return c.encipher(pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +423,7 @@ func (c *CipherSeed) BirthdayTime() time.Time {
|
|||||||
return BitcoinGenesisDate.Add(offset)
|
return BitcoinGenesisDate.Add(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mnemonic is a 24-word passphrase as of CipherSeedVersion zero. This
|
// Mnemonic is a 24-word passphrase as of cipher seed version zero. This
|
||||||
// passphrase encodes an encrypted seed triple (version, birthday, entropy).
|
// passphrase encodes an encrypted seed triple (version, birthday, entropy).
|
||||||
// Additionally, we also encode the salt used with scrypt to derive the key
|
// 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
|
// that the cipher text is encrypted with, and the version which tells us how
|
||||||
@@ -426,14 +464,16 @@ func mnemonicToCipherText(mnemonic *Mnemonic) [EncipheredCipherSeedSize]byte {
|
|||||||
func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) {
|
func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) {
|
||||||
// First, we'll attempt to decipher the mnemonic by mapping back into
|
// First, we'll attempt to decipher the mnemonic by mapping back into
|
||||||
// our byte slice and applying our deciphering scheme.
|
// our byte slice and applying our deciphering scheme.
|
||||||
plainSeed, err := m.Decipher(pass)
|
plainSeed, salt, err := m.Decipher(pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If decryption was successful, then we'll decode into a fresh
|
// If decryption was successful, then we'll decode into a fresh
|
||||||
// CipherSeed struct.
|
// CipherSeed struct.
|
||||||
var c CipherSeed
|
c := CipherSeed{
|
||||||
|
salt: salt,
|
||||||
|
}
|
||||||
if err := c.decode(bytes.NewReader(plainSeed[:])); err != nil {
|
if err := c.decode(bytes.NewReader(plainSeed[:])); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -445,36 +485,41 @@ func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) {
|
|||||||
// using the passed passphrase. This function is the opposite of
|
// using the passed passphrase. This function is the opposite of
|
||||||
// the encipher method.
|
// the encipher method.
|
||||||
func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte,
|
func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte,
|
||||||
pass []byte) ([DecipheredCipherSeedSize]byte, error) {
|
pass []byte) ([DecipheredCipherSeedSize]byte, [SaltSize]byte, error) {
|
||||||
|
|
||||||
var plainSeed [DecipheredCipherSeedSize]byte
|
var (
|
||||||
|
plainSeed [DecipheredCipherSeedSize]byte
|
||||||
|
salt [SaltSize]byte
|
||||||
|
)
|
||||||
|
|
||||||
// Before we do anything, we'll ensure that the version is one that we
|
// 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
|
// understand. Otherwise, we won't be able to decrypt, or even parse
|
||||||
// the cipher seed.
|
// the cipher seed.
|
||||||
if uint8(cipherSeedBytes[0]) != CipherSeedVersion {
|
if cipherSeedBytes[0] != CipherSeedVersion {
|
||||||
return plainSeed, ErrIncorrectVersion
|
return plainSeed, salt, ErrIncorrectVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, we'll slice off the salt from the pass cipher seed, then
|
// 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
|
// snip off the end of the cipher seed, ignoring the version, and
|
||||||
// finally the checksum.
|
// finally the checksum.
|
||||||
salt := cipherSeedBytes[saltOffset : saltOffset+saltSize]
|
copy(salt[:], cipherSeedBytes[saltOffset:saltOffset+SaltSize])
|
||||||
cipherSeed := cipherSeedBytes[1:saltOffset]
|
cipherSeed := cipherSeedBytes[1:saltOffset]
|
||||||
checksum := cipherSeedBytes[checkSumOffset:]
|
checksum := cipherSeedBytes[checkSumOffset:]
|
||||||
|
|
||||||
// Before we perform any crypto operations, we'll re-create and verify
|
// 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.
|
// the checksum to ensure that the user input the proper set of words.
|
||||||
freshChecksum := crc32.Checksum(cipherSeedBytes[:checkSumOffset], crcTable)
|
freshChecksum := crc32.Checksum(
|
||||||
|
cipherSeedBytes[:checkSumOffset], crcTable,
|
||||||
|
)
|
||||||
if freshChecksum != binary.BigEndian.Uint32(checksum) {
|
if freshChecksum != binary.BigEndian.Uint32(checksum) {
|
||||||
return plainSeed, ErrIncorrectMnemonic
|
return plainSeed, salt, ErrIncorrectMnemonic
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the salt separated from the cipher text, we'll now obtain the
|
// With the salt separated from the cipher text, we'll now obtain the
|
||||||
// key used for encryption.
|
// key used for encryption.
|
||||||
key, err := scrypt.Key(pass, salt, scryptN, scryptR, scryptP, keyLen)
|
key, err := scrypt.Key(pass, salt[:], scryptN, scryptR, scryptP, keyLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plainSeed, err
|
return plainSeed, salt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll also extract the AD that will be required to properly pass the
|
// We'll also extract the AD that will be required to properly pass the
|
||||||
@@ -488,18 +533,19 @@ func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte,
|
|||||||
key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil,
|
key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil,
|
||||||
)
|
)
|
||||||
if !ok {
|
if !ok {
|
||||||
return plainSeed, ErrInvalidPass
|
return plainSeed, salt, ErrInvalidPass
|
||||||
}
|
}
|
||||||
copy(plainSeed[:], plainSeedBytes)
|
copy(plainSeed[:], plainSeedBytes)
|
||||||
|
|
||||||
return plainSeed, nil
|
return plainSeed, salt, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decipher attempts to decipher the encoded mnemonic by first mapping to the
|
// Decipher attempts to decipher the encoded mnemonic by first mapping to the
|
||||||
// original ciphertext, then applying our deciphering scheme. ErrInvalidPass
|
// original ciphertext, then applying our deciphering scheme. ErrInvalidPass
|
||||||
// will be returned if the passphrase is incorrect.
|
// will be returned if the passphrase is incorrect.
|
||||||
func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error) {
|
func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte,
|
||||||
|
[SaltSize]byte, error) {
|
||||||
|
|
||||||
// Before we attempt to map the mnemonic back to the original
|
// Before we attempt to map the mnemonic back to the original
|
||||||
// ciphertext, we'll ensure that all the word are actually a part of
|
// ciphertext, we'll ensure that all the word are actually a part of
|
||||||
@@ -512,10 +558,11 @@ func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error)
|
|||||||
for i, word := range m {
|
for i, word := range m {
|
||||||
if _, ok := wordDict[word]; !ok {
|
if _, ok := wordDict[word]; !ok {
|
||||||
emptySeed := [DecipheredCipherSeedSize]byte{}
|
emptySeed := [DecipheredCipherSeedSize]byte{}
|
||||||
return emptySeed, ErrUnknownMnenomicWord{
|
return emptySeed, [SaltSize]byte{},
|
||||||
Word: word,
|
ErrUnknownMnemonicWord{
|
||||||
Index: uint8(i),
|
Word: word,
|
||||||
}
|
Index: uint8(i),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,20 +584,20 @@ func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ChangePass takes an existing mnemonic, and passphrase for said mnemonic and
|
// ChangePass takes an existing mnemonic, and passphrase for said mnemonic and
|
||||||
// re-enciphers the plaintext cipher seed into a brand new mnemonic. This can
|
// 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
|
// be used to allow users to re-encrypt the same seed with multiple pass
|
||||||
// phrases, or just change the passphrase on an existing seed.
|
// phrases, or just change the passphrase on an existing seed.
|
||||||
func (m *Mnemonic) ChangePass(oldPass, newPass []byte) (Mnemonic, error) {
|
func (m *Mnemonic) ChangePass(oldPass, newPass []byte) (Mnemonic, error) {
|
||||||
var newmnemonic Mnemonic
|
var newMnemonic Mnemonic
|
||||||
|
|
||||||
// First, we'll try to decrypt the current mnemonic using the existing
|
// First, we'll try to decrypt the current mnemonic using the existing
|
||||||
// passphrase. If this fails, then we can't proceed any further.
|
// passphrase. If this fails, then we can't proceed any further.
|
||||||
cipherSeed, err := m.ToCipherSeed(oldPass)
|
cipherSeed, err := m.ToCipherSeed(oldPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newmnemonic, err
|
return newMnemonic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the deciperhing was successful, then we'll now re-encipher using
|
// If the deciphering was successful, then we'll now re-encipher using
|
||||||
// the new user provided passphrase.
|
// the new user provided passphrase.
|
||||||
return cipherSeed.ToMnemonic(newPass)
|
return cipherSeed.ToMnemonic(newPass)
|
||||||
}
|
}
|
||||||
+141
-199
@@ -6,6 +6,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"testing/quick"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestVector defines the values that are used to create a fully initialized
|
// TestVector defines the values that are used to create a fully initialized
|
||||||
@@ -14,7 +16,7 @@ type TestVector struct {
|
|||||||
version uint8
|
version uint8
|
||||||
time time.Time
|
time time.Time
|
||||||
entropy [EntropySize]byte
|
entropy [EntropySize]byte
|
||||||
salt [saltSize]byte
|
salt [SaltSize]byte
|
||||||
password []byte
|
password []byte
|
||||||
expectedMnemonic [NumMnemonicWords]string
|
expectedMnemonic [NumMnemonicWords]string
|
||||||
expectedBirthday uint16
|
expectedBirthday uint16
|
||||||
@@ -27,56 +29,48 @@ var (
|
|||||||
0x0d, 0xe7, 0x95, 0xe4,
|
0x0d, 0xe7, 0x95, 0xe4,
|
||||||
0x1e, 0x0b, 0x4c, 0xfd,
|
0x1e, 0x0b, 0x4c, 0xfd,
|
||||||
}
|
}
|
||||||
testSalt = [saltSize]byte{
|
testSalt = [SaltSize]byte{
|
||||||
0x73, 0x61, 0x6c, 0x74, 0x31, // equal to "salt1"
|
0x73, 0x61, 0x6c, 0x74, 0x31, // equal to "salt1"
|
||||||
}
|
}
|
||||||
version0TestVectors = []TestVector{
|
version0TestVectors = []TestVector{{
|
||||||
{
|
version: 0,
|
||||||
version: 0,
|
time: BitcoinGenesisDate,
|
||||||
time: BitcoinGenesisDate,
|
entropy: testEntropy,
|
||||||
entropy: testEntropy,
|
salt: testSalt,
|
||||||
salt: testSalt,
|
password: []byte{},
|
||||||
password: []byte{},
|
expectedMnemonic: [NumMnemonicWords]string{
|
||||||
expectedMnemonic: [NumMnemonicWords]string{
|
"ability", "liquid", "travel", "stem", "barely", "drastic",
|
||||||
"ability", "liquid", "travel", "stem", "barely", "drastic",
|
"pact", "cupboard", "apple", "thrive", "morning", "oak",
|
||||||
"pact", "cupboard", "apple", "thrive", "morning", "oak",
|
"feature", "tissue", "couch", "old", "math", "inform",
|
||||||
"feature", "tissue", "couch", "old", "math", "inform",
|
"success", "suggest", "drink", "motion", "know", "royal",
|
||||||
"success", "suggest", "drink", "motion", "know", "royal",
|
|
||||||
},
|
|
||||||
expectedBirthday: 0,
|
|
||||||
},
|
},
|
||||||
{
|
expectedBirthday: 0,
|
||||||
version: 0,
|
}, {
|
||||||
time: time.Unix(1521799345, 0), // 03/23/2018 @ 10:02am (UTC)
|
version: 0,
|
||||||
entropy: testEntropy,
|
time: time.Unix(1521799345, 0), // 03/23/2018 @ 10:02am (UTC)
|
||||||
salt: testSalt,
|
entropy: testEntropy,
|
||||||
password: []byte("!very_safe_55345_password*"),
|
salt: testSalt,
|
||||||
expectedMnemonic: [NumMnemonicWords]string{
|
password: []byte("!very_safe_55345_password*"),
|
||||||
"able", "tree", "stool", "crush", "transfer", "cloud",
|
expectedMnemonic: [NumMnemonicWords]string{
|
||||||
"cross", "three", "profit", "outside", "hen", "citizen",
|
"able", "tree", "stool", "crush", "transfer", "cloud",
|
||||||
"plate", "ride", "require", "leg", "siren", "drum",
|
"cross", "three", "profit", "outside", "hen", "citizen",
|
||||||
"success", "suggest", "drink", "require", "fiscal", "upgrade",
|
"plate", "ride", "require", "leg", "siren", "drum",
|
||||||
},
|
"success", "suggest", "drink", "require", "fiscal", "upgrade",
|
||||||
expectedBirthday: 3365,
|
|
||||||
},
|
},
|
||||||
}
|
expectedBirthday: 3365,
|
||||||
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed,
|
func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed,
|
||||||
cipherSeed2 *CipherSeed) {
|
cipherSeed2 *CipherSeed) {
|
||||||
|
|
||||||
if cipherSeed.InternalVersion != cipherSeed2.InternalVersion {
|
require.Equal(
|
||||||
t.Fatalf("mismatched versions: expected %v, got %v",
|
t, cipherSeed.InternalVersion, cipherSeed2.InternalVersion,
|
||||||
cipherSeed.InternalVersion, cipherSeed2.InternalVersion)
|
"internal version",
|
||||||
}
|
)
|
||||||
if cipherSeed.Birthday != cipherSeed2.Birthday {
|
require.Equal(t, cipherSeed.Birthday, cipherSeed2.Birthday, "birthday")
|
||||||
t.Fatalf("mismatched birthday: expected %v, got %v",
|
require.Equal(t, cipherSeed.Entropy, cipherSeed2.Entropy, "entropy")
|
||||||
cipherSeed.Birthday, cipherSeed2.Birthday)
|
require.Equal(t, cipherSeed.salt, cipherSeed2.salt, "salt")
|
||||||
}
|
|
||||||
if cipherSeed.Entropy != cipherSeed2.Entropy {
|
|
||||||
t.Fatalf("mismatched versions: expected %x, got %x",
|
|
||||||
cipherSeed.Entropy[:], cipherSeed2.Entropy[:])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAezeedVersion0TestVectors tests some fixed test vector values against
|
// TestAezeedVersion0TestVectors tests some fixed test vector values against
|
||||||
@@ -84,41 +78,60 @@ func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed,
|
|||||||
func TestAezeedVersion0TestVectors(t *testing.T) {
|
func TestAezeedVersion0TestVectors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// To minimize the number of tests that need to be run,
|
// To minimize the number of tests that need to be run, go through all
|
||||||
// go through all test vectors in the same test and also check
|
// test vectors in the same test and also check the birthday calculation
|
||||||
// the birthday calculation while we're at it.
|
// while we're at it.
|
||||||
for _, v := range version0TestVectors {
|
for _, v := range version0TestVectors {
|
||||||
// First, we create new cipher seed with the given values
|
// First, we create new cipher seed with the given values
|
||||||
// from the test vector.
|
// from the test vector.
|
||||||
cipherSeed, err := New(v.version, &v.entropy, v.time)
|
cipherSeed, err := New(v.version, &v.entropy, v.time)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then we need to set the salt to the pre-defined value, otherwise
|
// Then we need to set the salt to the pre-defined value,
|
||||||
// we'll end up with randomness in our mnemonics.
|
// otherwise we'll end up with randomness in our mnemonics.
|
||||||
cipherSeed.salt = testSalt
|
cipherSeed.salt = v.salt
|
||||||
|
|
||||||
// Now that the seed has been created, we'll attempt to convert it to a
|
// Now that the seed has been created, we'll attempt to convert
|
||||||
// valid mnemonic.
|
// it to a valid mnemonic.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(v.password)
|
mnemonic, err := cipherSeed.ToMnemonic(v.password)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally we compare the generated mnemonic and birthday to the
|
// Finally we compare the generated mnemonic and birthday to the
|
||||||
// expected value.
|
// expected value.
|
||||||
if mnemonic != v.expectedMnemonic {
|
require.Equal(t, v.expectedMnemonic[:], mnemonic[:])
|
||||||
t.Fatalf("mismatched mnemonic: expected %s, got %s",
|
require.Equal(t, v.expectedBirthday, cipherSeed.Birthday)
|
||||||
v.expectedMnemonic, mnemonic)
|
|
||||||
}
|
|
||||||
if cipherSeed.Birthday != v.expectedBirthday {
|
|
||||||
t.Fatalf("mismatched birthday: expected %v, got %v",
|
|
||||||
v.expectedBirthday, cipherSeed.Birthday)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithRandomnessSource tests that seed generation is fully deterministic
|
||||||
|
// when a custom static randomness source is provided.
|
||||||
|
func TestWithRandomnessSource(t *testing.T) {
|
||||||
|
sourceData := append([]byte{}, testEntropy[:]...)
|
||||||
|
sourceData = append(sourceData, testSalt[:]...)
|
||||||
|
src := bytes.NewReader(sourceData)
|
||||||
|
|
||||||
|
// First, we create new cipher seed with the given values from the test
|
||||||
|
// vector but with no entropy.
|
||||||
|
v := version0TestVectors[0]
|
||||||
|
cipherSeed, err := New(
|
||||||
|
v.version, nil, v.time, WithRandomnessSource(src),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The salt should be set to our test salt.
|
||||||
|
require.Equal(t, testSalt, cipherSeed.salt)
|
||||||
|
|
||||||
|
// Now that the seed has been created, we'll attempt to convert it to a
|
||||||
|
// valid mnemonic.
|
||||||
|
mnemonic, err := cipherSeed.ToMnemonic(v.password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Finally, we compare the generated mnemonic and birthday to the
|
||||||
|
// expected value.
|
||||||
|
require.Equal(t, v.expectedMnemonic[:], mnemonic[:])
|
||||||
|
require.Equal(t, v.expectedBirthday, cipherSeed.Birthday)
|
||||||
|
}
|
||||||
|
|
||||||
// TestEmptyPassphraseDerivation tests that the aezeed scheme is able to derive
|
// TestEmptyPassphraseDerivation tests that the aezeed scheme is able to derive
|
||||||
// a proper mnemonic, and decipher that mnemonic when the user uses an empty
|
// a proper mnemonic, and decipher that mnemonic when the user uses an empty
|
||||||
// passphrase.
|
// passphrase.
|
||||||
@@ -131,23 +144,17 @@ func TestEmptyPassphraseDerivation(t *testing.T) {
|
|||||||
// We'll now create a new cipher seed with an internal version of zero
|
// We'll now create a new cipher seed with an internal version of zero
|
||||||
// to simulate a wallet that just adopted the scheme.
|
// to simulate a wallet that just adopted the scheme.
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the seed has been created, we'll attempt to convert it to a
|
// Now that the seed has been created, we'll attempt to convert it to a
|
||||||
// valid mnemonic.
|
// valid mnemonic.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll try to decrypt the mnemonic with the passphrase that we
|
// Next, we'll try to decrypt the mnemonic with the passphrase that we
|
||||||
// used.
|
// used.
|
||||||
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
|
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to decrypt mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll ensure that the uncovered cipher seed matches
|
// Finally, we'll ensure that the uncovered cipher seed matches
|
||||||
// precisely.
|
// precisely.
|
||||||
@@ -165,23 +172,17 @@ func TestManualEntropyGeneration(t *testing.T) {
|
|||||||
// We'll now create a new cipher seed with an internal version of zero
|
// We'll now create a new cipher seed with an internal version of zero
|
||||||
// to simulate a wallet that just adopted the scheme.
|
// to simulate a wallet that just adopted the scheme.
|
||||||
cipherSeed, err := New(0, nil, time.Now())
|
cipherSeed, err := New(0, nil, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the seed has been created, we'll attempt to convert it to a
|
// Now that the seed has been created, we'll attempt to convert it to a
|
||||||
// valid mnemonic.
|
// valid mnemonic.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll try to decrypt the mnemonic with the passphrase that we
|
// Next, we'll try to decrypt the mnemonic with the passphrase that we
|
||||||
// used.
|
// used.
|
||||||
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
|
cipherSeed2, err := mnemonic.ToCipherSeed(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to decrypt mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll ensure that the uncovered cipher seed matches
|
// Finally, we'll ensure that the uncovered cipher seed matches
|
||||||
// precisely.
|
// precisely.
|
||||||
@@ -189,7 +190,7 @@ func TestManualEntropyGeneration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestInvalidPassphraseRejection tests if a caller attempts to use the
|
// TestInvalidPassphraseRejection tests if a caller attempts to use the
|
||||||
// incorrect passprhase for an enciphered seed, then the proper error is
|
// incorrect passphrase for an enciphered seed, then the proper error is
|
||||||
// returned.
|
// returned.
|
||||||
func TestInvalidPassphraseRejection(t *testing.T) {
|
func TestInvalidPassphraseRejection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -197,23 +198,18 @@ func TestInvalidPassphraseRejection(t *testing.T) {
|
|||||||
// First, we'll generate a new cipher seed with a test passphrase.
|
// First, we'll generate a new cipher seed with a test passphrase.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have our cipher seed, we'll encipher it and request a
|
// Now that we have our cipher seed, we'll encipher it and request a
|
||||||
// mnemonic that we can use to recover later.
|
// mnemonic that we can use to recover later.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we try to decipher with the wrong passphrase, we should get the
|
// If we try to decipher with the wrong passphrase, we should get the
|
||||||
// proper error.
|
// proper error.
|
||||||
wrongPass := []byte("kek")
|
wrongPass := []byte("kek")
|
||||||
if _, err := mnemonic.ToCipherSeed(wrongPass); err != ErrInvalidPass {
|
_, err = mnemonic.ToCipherSeed(wrongPass)
|
||||||
t.Fatalf("expected ErrInvalidPass, instead got %v", err)
|
require.Equal(t, ErrInvalidPass, err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRawEncipherDecipher tests that callers are able to use the raw methods
|
// TestRawEncipherDecipher tests that callers are able to use the raw methods
|
||||||
@@ -224,36 +220,29 @@ func TestRawEncipherDecipher(t *testing.T) {
|
|||||||
// First, we'll generate a new cipher seed with a test passphrase.
|
// First, we'll generate a new cipher seed with a test passphrase.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the cipherseed obtained, we'll now use the raw encipher method
|
// With the cipher seed obtained, we'll now use the raw encipher method
|
||||||
// to obtain our final cipher text.
|
// to obtain our final cipher text.
|
||||||
cipherText, err := cipherSeed.Encipher(pass)
|
cipherText, err := cipherSeed.Encipher(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to encipher seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mnemonic, err := cipherTextToMnemonic(cipherText)
|
mnemonic, err := cipherTextToMnemonic(cipherText)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the ciphertext (mapped to the mnemonic), we'll
|
// Now that we have the ciphertext (mapped to the mnemonic), we'll
|
||||||
// attempt to decipher it raw using the user's passphrase.
|
// attempt to decipher it raw using the user's passphrase.
|
||||||
plainSeedBytes, err := mnemonic.Decipher(pass)
|
plainSeedBytes, salt, err := mnemonic.Decipher(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to decipher: %v", err)
|
require.Equal(t, cipherSeed.salt, salt)
|
||||||
}
|
|
||||||
|
|
||||||
// If we deserialize the plaintext seed bytes, it should exactly match
|
// If we deserialize the plaintext seed bytes, it should exactly match
|
||||||
// the original cipher seed.
|
// the original cipher seed.
|
||||||
var newSeed CipherSeed
|
newSeed := CipherSeed{
|
||||||
err = newSeed.decode(bytes.NewReader(plainSeedBytes[:]))
|
salt: salt,
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to decode cipher seed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
err = newSeed.decode(bytes.NewReader(plainSeedBytes[:]))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
assertCipherSeedEqual(t, cipherSeed, &newSeed)
|
assertCipherSeedEqual(t, cipherSeed, &newSeed)
|
||||||
}
|
}
|
||||||
@@ -266,17 +255,13 @@ func TestInvalidExternalVersion(t *testing.T) {
|
|||||||
|
|
||||||
// First, we'll generate a new cipher seed.
|
// First, we'll generate a new cipher seed.
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the cipherseed obtained, we'll now use the raw encipher method
|
// With the cipher seed obtained, we'll now use the raw encipher method
|
||||||
// to obtain our final cipher text.
|
// to obtain our final cipher text.
|
||||||
pass := []byte("newpasswhodis")
|
pass := []byte("newpasswhodis")
|
||||||
cipherText, err := cipherSeed.Encipher(pass)
|
cipherText, err := cipherSeed.Encipher(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to encipher seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the cipher text, we'll modify the first byte to be
|
// Now that we have the cipher text, we'll modify the first byte to be
|
||||||
// an invalid version.
|
// an invalid version.
|
||||||
@@ -284,11 +269,8 @@ func TestInvalidExternalVersion(t *testing.T) {
|
|||||||
|
|
||||||
// With the version swapped, if we try to decipher it, (no matter the
|
// With the version swapped, if we try to decipher it, (no matter the
|
||||||
// passphrase), it should fail.
|
// passphrase), it should fail.
|
||||||
_, err = decipherCipherSeed(cipherText, []byte("kek"))
|
_, _, err = decipherCipherSeed(cipherText, []byte("kek"))
|
||||||
if err != ErrIncorrectVersion {
|
require.Equal(t, ErrIncorrectVersion, err)
|
||||||
t.Fatalf("wrong error: expected ErrIncorrectVersion, "+
|
|
||||||
"got %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChangePassphrase tests that we're able to generate a cipher seed, then
|
// TestChangePassphrase tests that we're able to generate a cipher seed, then
|
||||||
@@ -300,31 +282,23 @@ func TestChangePassphrase(t *testing.T) {
|
|||||||
// First, we'll generate a new cipher seed with a test passphrase.
|
// First, we'll generate a new cipher seed with a test passphrase.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have our cipher seed, we'll encipher it and request a
|
// Now that we have our cipher seed, we'll encipher it and request a
|
||||||
// mnemonic that we can use to recover later.
|
// mnemonic that we can use to recover later.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that have the mnemonic, we'll attempt to re-encipher the
|
// Now that have the mnemonic, we'll attempt to re-encipher the
|
||||||
// passphrase in order to get a brand new mnemonic.
|
// passphrase in order to get a brand-new mnemonic.
|
||||||
newPass := []byte("strongerpassyeh!")
|
newPass := []byte("strongerpassyeh!")
|
||||||
newmnemonic, err := mnemonic.ChangePass(pass, newPass)
|
newMnemonic, err := mnemonic.ChangePass(pass, newPass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to change passphrase: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll now attempt to decipher the new mnemonic using the new
|
// We'll now attempt to decipher the new mnemonic using the new
|
||||||
// passphrase to arrive at (what should be) the original cipher seed.
|
// passphrase to arrive at (what should be) the original cipher seed.
|
||||||
newCipherSeed, err := newmnemonic.ToCipherSeed(newPass)
|
newCipherSeed, err := newMnemonic.ToCipherSeed(newPass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to decipher cipher seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the cipher seed, we'll verify that the plaintext
|
// Now that we have the cipher seed, we'll verify that the plaintext
|
||||||
// seed matches *identically*.
|
// seed matches *identically*.
|
||||||
@@ -332,7 +306,7 @@ func TestChangePassphrase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestChangePassphraseWrongPass tests that if we have a valid enciphered
|
// TestChangePassphraseWrongPass tests that if we have a valid enciphered
|
||||||
// cipherseed, but then try to change the password with the *wrong* password,
|
// cipher seed, but then try to change the password with the *wrong* password,
|
||||||
// then we get an error.
|
// then we get an error.
|
||||||
func TestChangePassphraseWrongPass(t *testing.T) {
|
func TestChangePassphraseWrongPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -340,27 +314,21 @@ func TestChangePassphraseWrongPass(t *testing.T) {
|
|||||||
// First, we'll generate a new cipher seed with a test passphrase.
|
// First, we'll generate a new cipher seed with a test passphrase.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have our cipher seed, we'll encipher it and request a
|
// Now that we have our cipher seed, we'll encipher it and request a
|
||||||
// mnemonic that we can use to recover later.
|
// mnemonic that we can use to recover later.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that have the mnemonic, we'll attempt to re-encipher the
|
// 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
|
// passphrase in order to get a brand-new mnemonic. However, we'll be
|
||||||
// using the *wrong* passphrase. This should result in an
|
// using the *wrong* passphrase. This should result in an
|
||||||
// ErrInvalidPass error.
|
// ErrInvalidPass error.
|
||||||
wrongPass := []byte("kek")
|
wrongPass := []byte("kek")
|
||||||
newPass := []byte("strongerpassyeh!")
|
newPass := []byte("strongerpassyeh!")
|
||||||
_, err = mnemonic.ChangePass(wrongPass, newPass)
|
_, err = mnemonic.ChangePass(wrongPass, newPass)
|
||||||
if err != ErrInvalidPass {
|
require.Equal(t, ErrInvalidPass, err)
|
||||||
t.Fatalf("expected ErrInvalidPass, instead got %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMnemonicEncoding uses quickcheck like property based testing to ensure
|
// TestMnemonicEncoding uses quickcheck like property based testing to ensure
|
||||||
@@ -397,7 +365,7 @@ func TestMnemonicEncoding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestEncipherDecipher is a property-based test that ensures that given a
|
// 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
|
// version, entropy, and birthday, then we're able to map that to a cipher seed
|
||||||
// mnemonic, then back to the original plaintext cipher seed.
|
// mnemonic, then back to the original plaintext cipher seed.
|
||||||
func TestEncipherDecipher(t *testing.T) {
|
func TestEncipherDecipher(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -406,7 +374,7 @@ func TestEncipherDecipher(t *testing.T) {
|
|||||||
// ensure that given a random seed tuple (internal version, entropy,
|
// ensure that given a random seed tuple (internal version, entropy,
|
||||||
// and birthday) we're able to convert that to a valid cipher seed.
|
// and birthday) we're able to convert that to a valid cipher seed.
|
||||||
// Additionally, we should be able to decipher the final mnemonic, and
|
// Additionally, we should be able to decipher the final mnemonic, and
|
||||||
// recover the original cipherseed.
|
// recover the original cipher seed.
|
||||||
mainScenario := func(version uint8, entropy [EntropySize]byte,
|
mainScenario := func(version uint8, entropy [EntropySize]byte,
|
||||||
nowInt int64, pass [20]byte) bool {
|
nowInt int64, pass [20]byte) bool {
|
||||||
|
|
||||||
@@ -458,15 +426,17 @@ func TestEncipherDecipher(t *testing.T) {
|
|||||||
// arbitrary raw seed.
|
// arbitrary raw seed.
|
||||||
func TestSeedEncodeDecode(t *testing.T) {
|
func TestSeedEncodeDecode(t *testing.T) {
|
||||||
// mainScenario is the primary driver of our property-based test. We'll
|
// 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
|
// ensure that given a random cipher seed, we can encode it and decode
|
||||||
// it precisely.
|
// it precisely.
|
||||||
mainScenario := func(version uint8, nowInt int64,
|
mainScenario := func(version uint8, nowInt int64,
|
||||||
entropy [EntropySize]byte) bool {
|
entropy [EntropySize]byte) bool {
|
||||||
|
|
||||||
now := time.Unix(nowInt, 0)
|
now := time.Unix(nowInt, 0)
|
||||||
|
day := time.Hour * 24
|
||||||
|
numDaysSinceGenesis := now.Sub(BitcoinGenesisDate) / day
|
||||||
seed := CipherSeed{
|
seed := CipherSeed{
|
||||||
InternalVersion: version,
|
InternalVersion: version,
|
||||||
Birthday: uint16(now.Sub(BitcoinGenesisDate) / (time.Hour * 24)),
|
Birthday: uint16(numDaysSinceGenesis),
|
||||||
Entropy: entropy,
|
Entropy: entropy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,25 +476,21 @@ func TestSeedEncodeDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDecipherUnknownMnenomicWord tests that if we obtain a mnemonic, the
|
// TestDecipherUnknownMnemonicWord tests that if we obtain a mnemonic, then
|
||||||
// modify one of the words to not be within the word list, then it's detected
|
// 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.
|
// when we attempt to map it back to the original cipher seed.
|
||||||
func TestDecipherUnknownMnenomicWord(t *testing.T) {
|
func TestDecipherUnknownMnemonicWord(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// First, we'll create a new cipher seed with "test" ass a password.
|
// First, we'll create a new cipher seed with "test" ass a password.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have our cipher seed, we'll encipher it and request a
|
// Now that we have our cipher seed, we'll encipher it and request a
|
||||||
// mnemonic that we can use to recover later.
|
// mnemonic that we can use to recover later.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before we attempt to decrypt the cipher seed, we'll mutate one of
|
// 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.
|
// the word so it isn't actually in our final word list.
|
||||||
@@ -532,58 +498,36 @@ func TestDecipherUnknownMnenomicWord(t *testing.T) {
|
|||||||
mnemonic[randIndex] = "kek"
|
mnemonic[randIndex] = "kek"
|
||||||
|
|
||||||
// If we attempt to map back to the original cipher seed now, then we
|
// If we attempt to map back to the original cipher seed now, then we
|
||||||
// should get ErrUnknownMnenomicWord.
|
// should get ErrUnknownMnemonicWord.
|
||||||
_, err = mnemonic.ToCipherSeed(pass)
|
_, err = mnemonic.ToCipherSeed(pass)
|
||||||
if err == nil {
|
wordErr := &ErrUnknownMnemonicWord{}
|
||||||
t.Fatalf("expected ErrUnknownMnenomicWord error")
|
require.ErrorAs(t, err, wordErr)
|
||||||
}
|
require.Equal(t, "kek", wordErr.Word)
|
||||||
|
require.Equal(t, uint8(randIndex), wordErr.Index)
|
||||||
|
|
||||||
wordErr, ok := err.(ErrUnknownMnenomicWord)
|
// If the mnemonic includes a word that is not in the englishList it
|
||||||
if !ok {
|
// fails, even when it is a substring of a valid word Example: `heart`
|
||||||
t.Fatalf("expected ErrUnknownMnenomicWord instead got %T", err)
|
// is in the list, `hear` is not.
|
||||||
}
|
|
||||||
|
|
||||||
if wordErr.Word != "kek" {
|
|
||||||
t.Fatalf("word mismatch: expected %v, got %v", "kek", wordErr.Word)
|
|
||||||
}
|
|
||||||
if int32(wordErr.Index) != randIndex {
|
|
||||||
t.Fatalf("wrong index detected: expected %v, got %v",
|
|
||||||
randIndex, wordErr.Index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the mnemonic includes a word that is not in the englishList
|
|
||||||
// it fails, even when it is a substring of a valid word
|
|
||||||
// Example: `heart` is in the list, `hear` is not
|
|
||||||
mnemonic[randIndex] = "hear"
|
mnemonic[randIndex] = "hear"
|
||||||
|
|
||||||
// If we attempt to map back to the original cipher seed now, then we
|
// If we attempt to map back to the original cipher seed now, then we
|
||||||
// should get ErrUnknownMnenomicWord.
|
// should get ErrUnknownMnemonicWord.
|
||||||
_, err = mnemonic.ToCipherSeed(pass)
|
_, err = mnemonic.ToCipherSeed(pass)
|
||||||
if err == nil {
|
require.ErrorAs(t, err, wordErr)
|
||||||
t.Fatalf("expected ErrUnknownMnenomicWord error")
|
|
||||||
}
|
|
||||||
_, ok = err.(ErrUnknownMnenomicWord)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected ErrUnknownMnenomicWord instead got %T", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDecipherIncorrectMnemonic tests that if we obtain a cipherseed, but then
|
// TestDecipherIncorrectMnemonic tests that if we obtain a cipher seed, but then
|
||||||
// swap out words, then checksum fails.
|
// swap out words, then checksum fails.
|
||||||
func TestDecipherIncorrectMnemonic(t *testing.T) {
|
func TestDecipherIncorrectMnemonic(t *testing.T) {
|
||||||
// First, we'll create a new cipher seed with "test" ass a password.
|
// First, we'll create a new cipher seed with "test" ass a password.
|
||||||
pass := []byte("test")
|
pass := []byte("test")
|
||||||
cipherSeed, err := New(0, &testEntropy, time.Now())
|
cipherSeed, err := New(0, &testEntropy, time.Now())
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create seed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have our cipher seed, we'll encipher it and request a
|
// Now that we have our cipher seed, we'll encipher it and request a
|
||||||
// mnemonic that we can use to recover later.
|
// mnemonic that we can use to recover later.
|
||||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("unable to create mnemonic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll now swap out two words from the mnemonic, which should trigger
|
// We'll now swap out two words from the mnemonic, which should trigger
|
||||||
// a checksum failure.
|
// a checksum failure.
|
||||||
@@ -593,11 +537,9 @@ func TestDecipherIncorrectMnemonic(t *testing.T) {
|
|||||||
|
|
||||||
// If we attempt to decrypt now, we should get a checksum failure.
|
// 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
|
// If we attempt to map back to the original cipher seed now, then we
|
||||||
// should get ErrUnknownMnenomicWord.
|
// should get ErrIncorrectMnemonic.
|
||||||
_, err = mnemonic.ToCipherSeed(pass)
|
_, err = mnemonic.ToCipherSeed(pass)
|
||||||
if err != ErrIncorrectMnemonic {
|
require.Equal(t, ErrIncorrectMnemonic, err)
|
||||||
t.Fatalf("expected ErrIncorrectMnemonic error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): add test failure checksum fail is modified, new error
|
// TODO(roasbeef): add test failure checksum fail is modified, new error
|
||||||
|
|||||||
+4
-4
@@ -18,9 +18,9 @@ var (
|
|||||||
"match")
|
"match")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrUnknownMnenomicWord is returned when attempting to decipher and
|
// ErrUnknownMnemonicWord is returned when attempting to decipher and
|
||||||
// enciphered mnemonic, but a word encountered isn't a member of our word list.
|
// enciphered mnemonic, but a word encountered isn't a member of our word list.
|
||||||
type ErrUnknownMnenomicWord struct {
|
type ErrUnknownMnemonicWord struct {
|
||||||
// Word is the unknown word in the mnemonic phrase.
|
// Word is the unknown word in the mnemonic phrase.
|
||||||
Word string
|
Word string
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ type ErrUnknownMnenomicWord struct {
|
|||||||
Index uint8
|
Index uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns a human readable string describing the error.
|
// Error returns a human-readable string describing the error.
|
||||||
func (e ErrUnknownMnenomicWord) Error() string {
|
func (e ErrUnknownMnemonicWord) Error() string {
|
||||||
return fmt.Sprintf("word %v isn't a part of default word list "+
|
return fmt.Sprintf("word %v isn't a part of default word list "+
|
||||||
"(index=%v)", e.Word, e.Index)
|
"(index=%v)", e.Word, e.Index)
|
||||||
}
|
}
|
||||||
+3
-4
@@ -965,14 +965,13 @@ func waitForWalletPassword(cfg *Config,
|
|||||||
// seed. If it's greater than the current key derivation
|
// seed. If it's greater than the current key derivation
|
||||||
// version, then we'll return an error as we don't understand
|
// version, then we'll return an error as we don't understand
|
||||||
// this.
|
// this.
|
||||||
const latestVersion = keychain.KeyDerivationVersion
|
|
||||||
if cipherSeed != nil &&
|
if cipherSeed != nil &&
|
||||||
cipherSeed.InternalVersion != latestVersion {
|
!keychain.IsKnownVersion(cipherSeed.InternalVersion) {
|
||||||
|
|
||||||
return nil, fmt.Errorf("invalid internal "+
|
return nil, fmt.Errorf("invalid internal "+
|
||||||
"seed version %v, current version is %v",
|
"seed version %v, current max version is %v",
|
||||||
cipherSeed.InternalVersion,
|
cipherSeed.InternalVersion,
|
||||||
keychain.KeyDerivationVersion)
|
keychain.CurrentKeyDerivationVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
loader, err := btcwallet.NewWalletLoader(
|
loader, err := btcwallet.NewWalletLoader(
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ The `walletrpc.SignPsbt` RPC now also supports [Taproot PSBT
|
|||||||
signing](https://github.com/lightningnetwork/lnd/pull/6450) to fully support
|
signing](https://github.com/lightningnetwork/lnd/pull/6450) to fully support
|
||||||
remote signing with Taproot outputs.
|
remote signing with Taproot outputs.
|
||||||
|
|
||||||
|
The internal version of the `aezeed` [was bumped to `1` to mark new seeds that
|
||||||
|
were created after introducing the Taproot key
|
||||||
|
derivation](https://github.com/lightningnetwork/lnd/pull/6524) to simplify
|
||||||
|
detecting Taproot compatibility of a seed.
|
||||||
|
|
||||||
## MuSig2
|
## MuSig2
|
||||||
|
|
||||||
The [`signrpc.Signer` RPC service now supports EXPERIMENTAL MuSig2
|
The [`signrpc.Signer` RPC service now supports EXPERIMENTAL MuSig2
|
||||||
|
|||||||
+21
-5
@@ -8,11 +8,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// KeyDerivationVersion is the version of the key derivation schema
|
// KeyDerivationVersionLegacy is the previous version of the key
|
||||||
// defined below. We use a version as this means that we'll be able to
|
// derivation schema defined below. We use a version as this means that
|
||||||
// accept new seed in the future and be able to discern if the software
|
// we'll be able to accept new seed in the future and be able to discern
|
||||||
// is compatible with the version of the seed.
|
// if the software is compatible with the version of the seed.
|
||||||
KeyDerivationVersion = 0
|
KeyDerivationVersionLegacy = 0
|
||||||
|
|
||||||
|
// KeyDerivationVersionTaproot is the most recent version of the key
|
||||||
|
// derivation scheme that marks the introduction of the Taproot
|
||||||
|
// derivation with BIP0086 support.
|
||||||
|
KeyDerivationVersionTaproot = 1
|
||||||
|
|
||||||
|
// CurrentKeyDerivationVersion is the current default key derivation
|
||||||
|
// version that is used for new seeds.
|
||||||
|
CurrentKeyDerivationVersion = KeyDerivationVersionTaproot
|
||||||
|
|
||||||
// BIP0043Purpose is the "purpose" value that we'll use for the first
|
// BIP0043Purpose is the "purpose" value that we'll use for the first
|
||||||
// version or our key derivation scheme. All keys are expected to be
|
// version or our key derivation scheme. All keys are expected to be
|
||||||
@@ -25,6 +34,13 @@ const (
|
|||||||
BIP0043Purpose = 1017
|
BIP0043Purpose = 1017
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsKnownVersion returns true if the given version is one of the known
|
||||||
|
// derivation scheme versions as defined by this package.
|
||||||
|
func IsKnownVersion(internalVersion uint8) bool {
|
||||||
|
return internalVersion == KeyDerivationVersionLegacy ||
|
||||||
|
internalVersion == KeyDerivationVersionTaproot
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// MaxKeyRangeScan is the maximum number of keys that we'll attempt to
|
// MaxKeyRangeScan is the maximum number of keys that we'll attempt to
|
||||||
// scan with if a caller knows the public key, but not the KeyLocator
|
// scan with if a caller knows the public key, but not the KeyLocator
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ func (u *UnlockerService) GenSeed(_ context.Context,
|
|||||||
// instance.
|
// instance.
|
||||||
//
|
//
|
||||||
cipherSeed, err := aezeed.New(
|
cipherSeed, err := aezeed.New(
|
||||||
keychain.KeyDerivationVersion, &entropy, time.Now(),
|
keychain.CurrentKeyDerivationVersion, &entropy, time.Now(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func createSeedAndMnemonic(t *testing.T,
|
|||||||
pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) {
|
pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) {
|
||||||
|
|
||||||
cipherSeed, err := aezeed.New(
|
cipherSeed, err := aezeed.New(
|
||||||
keychain.KeyDerivationVersion, &testEntropy, time.Now(),
|
keychain.CurrentKeyDerivationVersion, &testEntropy, time.Now(),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user