From daa5966119761f4c68cc0a8f100267fd6f0b3749 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 12 May 2022 12:47:11 +0200 Subject: [PATCH] aezeed: make seed generation fully deterministic This commit fixes a TODO to make the seed generation fully deterministic by providing the option to pass in a custom randomness source. --- aezeed/cipherseed.go | 44 +++++++++++++++++++++++++++++++++------ aezeed/cipherseed_test.go | 29 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/aezeed/cipherseed.go b/aezeed/cipherseed.go index db5b224c6..4e05b25e0 100644 --- a/aezeed/cipherseed.go +++ b/aezeed/cipherseed.go @@ -128,6 +128,33 @@ var ( 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 // 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 @@ -174,17 +201,22 @@ type CipherSeed struct { // 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. +// 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, - now time.Time) (*CipherSeed, error) { + now time.Time, modifiers ...SeedOptionModifier) (*CipherSeed, error) { - // TODO(roasbeef): pass randomness source? to make fully deterministic? + opts := DefaultOptions() + for _, modifier := range modifiers { + modifier(opts) + } // 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 if entropy == nil { - if _, err := rand.Read(seed[:]); err != nil { + if _, err := opts.randomnessSource.Read(seed[:]); err != nil { return nil, err } } 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 // eventually derive our key. - if _, err := rand.Read(c.salt[:]); err != nil { + if _, err := opts.randomnessSource.Read(c.salt[:]); err != nil { return nil, err } diff --git a/aezeed/cipherseed_test.go b/aezeed/cipherseed_test.go index 3d1acf40c..7547f25e9 100644 --- a/aezeed/cipherseed_test.go +++ b/aezeed/cipherseed_test.go @@ -103,6 +103,35 @@ func TestAezeedVersion0TestVectors(t *testing.T) { } } +// 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 // a proper mnemonic, and decipher that mnemonic when the user uses an empty // passphrase.