aezeed

package
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2024 License: MIT Imports: 11 Imported by: 0

README

aezeed

In this PR we added a new package implementing the aezeed cipher seed scheme (based on aez).

This new scheme aims to address two major features lacking in BIP39: versioning, and a wallet birthday. The lack a version means that wallets may not necessarily know how to re-derive addresses during the recovery process. A lack of a birthday means that wallets don’t know how far back to look in the chain to ensure that they derive all the proper user addresses. Additionally, BIP39 use a very weak KDF. We use scrypt with modern parameters (n=32768, r=8, p=1). A set of benchmarks has been added, on my laptop I get about 100ms per attempt):

$  go test -run=XXX -bench=.

goos: linux
goarch: amd64
pkg: github.com/lightningnetwork/lnd/aezeed
BenchmarkTomnemonic-4                 20          93280730 ns/op        33559670 B/op         36 allocs/op
BenchmarkToCipherSeed-4               10         102323892 ns/op        36915684 B/op         41 allocs/op
PASS
ok      github.com/lightningnetwork/lnd/aezeed  4.168s

Aside from addressing the shortcomings of BIP 39, an aezeed cipher seed can both be upgraded, and have its password changed.

Sample seed:

ability dance scatter raw fly dentist bar nominee exhaust wine snap super cost case coconut ticket spread funny grain chimney aspect business quiz ginger

Plaintext aezeed encoding

The aezeed scheme addresses these two drawbacks and adds a number of desirable features. First, we start with the following plaintext seed:

1 byte internal version || 2 byte timestamp || 16 bytes of entropy

The version field is for wallets to be able to know how to re-derive the keys of the wallet.

The 2 byte timestamp is expressed in Bitcoin Days Genesis, meaning that the number of days since the timestamp in Bitcoin’s genesis block. This allow us to save space, and also avoid using a wasteful level of granularity. This can currently express time up until 2188.

Finally, the entropy is raw entropy that should be used to derive the wallet’s HD root.

aezeed enciphering/deciperhing

Next, we’ll take the plaintext seed described above and encipher it to procure a final cipher text. We’ll then take this cipher text (the CipherSeed) and encode that using a 24-word mnemonic. The enciphering process takes a user-defined passphrase. If no passphrase is provided, then the string “aezeed” will be used.

To encipher a plaintext seed (19 bytes) to arrive at an enciphered cipher seed (33 bytes), we apply the following operations:

  • First we take the external version and append it to our buffer. The external version describes how we encipher. For the first version (version 0), we’ll use scrypt(n=32768, r=8, p=1) and aezeed.
  • Next, we’ll use scrypt (with the version 9 params) to generate a strong key for encryption. We’ll generate a 32-byte key using 5 bytes as a salt. The usage of the salt is meant to make the creation of rainbow tables infeasible.
  • Next, the enciphering process. We use aez, modern AEAD with nonce-misuse resistance properties. The important trait we exploit is that it’s an arbitrary input length block cipher. Additionally, it has what’s essentially a configurable MAC size. In our scheme we’ll use a value of 8, which acts as a 64-bit checksum. We’ll encrypt with our generated seed, and use an AD of (version || salt).
  • Finally, we’ll encode this 33-byte cipher text using the default word list of BIP 39 to produce 24 English words.

Properties of the aezeed cipher seed

The aezeed cipher seed scheme has a few cool properties, notably:

  • The mnemonic itself is a cipher text, meaning leaving it in plaintext is advisable if the user also sets a passphrase. This is in contrast to BIP 39 where the mnemonic alone (without a passphrase) may be sufficient to steal funds.
  • A cipherseed can be modified to change the passphrase. This means that if the users wants a stronger passphrase, they can decipher (with the old passphrase), then encipher (with a new passphrase). Compared to BIP 39, where if the users used a passphrase, since the mapping is one way, they can’t change the passphrase of their existing HD key chain.
  • A cipher seed can be upgraded. Since we have an external version, offline tools can be provided to decipher using the old params, and encipher using the new params. In the future if we change ciphers, change scrypt, or just the parameters of scrypt, then users can easily upgrade their seed with an offline tool.

Documentation

Index

Constants

View Source
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 to 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

	// 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
)

Variables

View Source
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")
)
View Source
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)
)
View Source
var DefaultWordList = strings.Split(englishWordList, "\n")

DefaultWordList is a slice of the current default word list that's used to encode the enciphered seed into a human readable set of words.

View Source
var (
	// ReverseWordMap maps a word to its position within the default word list.
	ReverseWordMap map[string]int
)

Functions

This section is empty.

Types

type CipherSeed

type CipherSeed struct {
	// InternalVersion is the version of the plaintext cipher seed. 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
	// contains filtered or unexported fields
}

CipherSeed is a fully decoded instance of the aezeed scheme. At a high 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 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.

func New

func New(internalVersion uint8, entropy *[EntropySize]byte,
	now time.Time, modifiers ...SeedOptionModifier) (*CipherSeed, error)

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 fixed argument should be the time at which the seed was created, followed by optional seed option modifiers.

func (*CipherSeed) BirthdayTime

func (c *CipherSeed) BirthdayTime() time.Time

BirthdayTime returns the cipher seed's internal birthday format as a native golang Time struct.

func (*CipherSeed) Encipher

func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte,
	error)

Encipher maps the cipher seed to an aez ciphertext using an optional passphrase.

func (*CipherSeed) ToMnemonic

func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error)

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.

type ErrUnknownMnemonicWord

type ErrUnknownMnemonicWord struct {
	// Word is the unknown word in the mnemonic phrase.
	Word string

	// Index is the index (starting from zero) within the slice of strings
	// that makes up the mnemonic that points to the incorrect word.
	Index uint8
}

ErrUnknownMnemonicWord is returned when attempting to decipher and enciphered mnemonic, but a word encountered isn't a member of our word list.

func (ErrUnknownMnemonicWord) Error

func (e ErrUnknownMnemonicWord) Error() string

Error returns a human-readable string describing the error.

type Mnemonic

type Mnemonic [NumMnemonicWords]string

Mnemonic is a 24-word passphrase as of cipher seed version 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.

func (*Mnemonic) ChangePass

func (m *Mnemonic) ChangePass(oldPass, newPass []byte) (Mnemonic, error)

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 (*Mnemonic) Decipher

func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte,
	[SaltSize]byte, error)

Decipher attempts to decipher the encoded mnemonic by first mapping to the original ciphertext, then applying our deciphering scheme. ErrInvalidPass will be returned if the passphrase is incorrect.

func (*Mnemonic) ToCipherSeed

func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error)

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.

type SeedOptionModifier

type SeedOptionModifier func(*SeedOptions)

SeedOptionModifier is a function signature for modifying the default SeedOptions.

func WithRandomnessSource

func WithRandomnessSource(src io.Reader) SeedOptionModifier

WithRandomnessSource returns an option modifier that replaces the default randomness source with the given reader.

type SeedOptions

type SeedOptions struct {
	// contains filtered or unexported fields
}

SeedOptions is a type that holds options that configure the generation of a new cipher seed.

func DefaultOptions

func DefaultOptions() *SeedOptions

DefaultOptions returns the default seed options.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL