From 6aa3072403d1ee7e4030f3c7c9a0e2a22e194fc0 Mon Sep 17 00:00:00 2001 From: Thibault NORMAND Date: Wed, 5 Jul 2023 15:18:38 +0200 Subject: [PATCH] feat: increase scrypt parameters (#470) * feat(encrypted): adaptative scrypt parameters Signed-off-by: Thibault Normand * feat(encrypted): ensure standard parameters by default Signed-off-by: Thibault Normand * test(encrypted): add vector tests Signed-off-by: Thibault Normand --------- Signed-off-by: Thibault Normand Co-authored-by: Radoslav Dimitrov --- encrypted/encrypted.go | 102 +++++++++++++++++++++++++++++------- encrypted/encrypted_test.go | 95 +++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 19 deletions(-) diff --git a/encrypted/encrypted.go b/encrypted/encrypted.go index 4d174d61..037a718a 100644 --- a/encrypted/encrypted.go +++ b/encrypted/encrypted.go @@ -23,13 +23,46 @@ const ( boxNonceSize = 24 ) +// KDFParameterStrength defines the KDF parameter strength level to be used for +// encryption key derivation. +type KDFParameterStrength uint8 + const ( - // N parameter was chosen to be ~100ms of work using the default implementation - // on the 2.3GHz Core i7 Haswell processor in a late-2013 Apple Retina Macbook - // Pro (it takes ~113ms). - scryptN = 32768 - scryptR = 8 - scryptP = 1 + // Legacy defines legacy scrypt parameters (N:2^15, r:8, p:1) + Legacy KDFParameterStrength = iota + 1 + // Standard defines standard scrypt parameters which is focusing 100ms of computation (N:2^16, r:8, p:1) + Standard + // OWASP defines OWASP recommended scrypt parameters (N:2^17, r:8, p:1) + OWASP +) + +var ( + // legacyParams represents old scrypt derivation parameters for backward + // compatibility. + legacyParams = scryptParams{ + N: 32768, // 2^15 + R: 8, + P: 1, + } + + // standardParams defines scrypt parameters based on the scrypt creator + // recommendation to limit key derivation in time boxed to 100ms. + standardParams = scryptParams{ + N: 65536, // 2^16 + R: 8, + P: 1, + } + + // owaspParams defines scrypt parameters recommended by OWASP + owaspParams = scryptParams{ + N: 131072, // 2^17 + R: 8, + P: 1, + } + + // defaultParams defines scrypt parameters which will be used to generate a + // new key. + defaultParams = standardParams ) const ( @@ -49,19 +82,33 @@ type scryptParams struct { P int `json:"p"` } -func newScryptKDF() (scryptKDF, error) { +func (sp *scryptParams) Equal(in *scryptParams) bool { + return in != nil && sp.N == in.N && sp.P == in.P && sp.R == in.R +} + +func newScryptKDF(level KDFParameterStrength) (scryptKDF, error) { salt := make([]byte, saltSize) if err := fillRandom(salt); err != nil { - return scryptKDF{}, err + return scryptKDF{}, fmt.Errorf("unable to generate a random salt: %w", err) + } + + var params scryptParams + switch level { + case Legacy: + params = legacyParams + case Standard: + params = standardParams + case OWASP: + params = owaspParams + default: + // Fallback to default parameters + params = defaultParams } + return scryptKDF{ - Name: nameScrypt, - Params: scryptParams{ - N: scryptN, - R: scryptR, - P: scryptP, - }, - Salt: salt, + Name: nameScrypt, + Params: params, + Salt: salt, }, nil } @@ -79,9 +126,14 @@ func (s *scryptKDF) Key(passphrase []byte) ([]byte, error) { // be. If we do not do this, an attacker could cause a DoS by tampering with // them. func (s *scryptKDF) CheckParams() error { - if s.Params.N != scryptN || s.Params.R != scryptR || s.Params.P != scryptP { - return errors.New("encrypted: unexpected kdf parameters") + switch { + case legacyParams.Equal(&s.Params): + case standardParams.Equal(&s.Params): + case owaspParams.Equal(&s.Params): + default: + return errors.New("unsupported scrypt parameters") } + return nil } @@ -151,7 +203,14 @@ func (s *secretBoxCipher) Decrypt(ciphertext, key []byte) ([]byte, error) { // Encrypt takes a passphrase and plaintext, and returns a JSON object // containing ciphertext and the details necessary to decrypt it. func Encrypt(plaintext, passphrase []byte) ([]byte, error) { - k, err := newScryptKDF() + return EncryptWithCustomKDFParameters(plaintext, passphrase, Standard) +} + +// EncryptWithCustomKDFParameters takes a passphrase, the plaintext and a KDF +// parameter level (Legacy, Standard, or OWASP), and returns a JSON object +// containing ciphertext and the details necessary to decrypt it. +func EncryptWithCustomKDFParameters(plaintext, passphrase []byte, kdfLevel KDFParameterStrength) ([]byte, error) { + k, err := newScryptKDF(kdfLevel) if err != nil { return nil, err } @@ -176,11 +235,16 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) { // Marshal encrypts the JSON encoding of v using passphrase. func Marshal(v interface{}, passphrase []byte) ([]byte, error) { + return MarshalWithCustomKDFParameters(v, passphrase, Standard) +} + +// MarshalWithCustomKDFParameters encrypts the JSON encoding of v using passphrase. +func MarshalWithCustomKDFParameters(v interface{}, passphrase []byte, kdfLevel KDFParameterStrength) ([]byte, error) { data, err := json.MarshalIndent(v, "", "\t") if err != nil { return nil, err } - return Encrypt(data, passphrase) + return EncryptWithCustomKDFParameters(data, passphrase, kdfLevel) } // Decrypt takes a JSON-encoded ciphertext object encrypted using Encrypt and diff --git a/encrypted/encrypted_test.go b/encrypted/encrypted_test.go index b9ab9c57..bdffff9e 100644 --- a/encrypted/encrypted_test.go +++ b/encrypted/encrypted_test.go @@ -2,11 +2,20 @@ package encrypted import ( "encoding/json" + "strings" "testing" . "gopkg.in/check.v1" ) +var ( + kdfVectors = map[KDFParameterStrength][]byte{ + Legacy: []byte(`{"kdf":{"name":"scrypt","params":{"N":32768,"r":8,"p":1},"salt":"WO3mVvyTwJ9vwT5/Tk5OW5WPIBUofMjcpEfrLnfY4uA="},"cipher":{"name":"nacl/secretbox","nonce":"tCy7HcTFr4uxv4Nrg/DWmncuZ148U1MX"},"ciphertext":"08n43p5G5yviPEZpO7tPPF4aZQkWiWjkv4taFdhDBA0tamKH4nw="}`), + Standard: []byte(`{"kdf":{"name":"scrypt","params":{"N":65536,"r":8,"p":1},"salt":"FhzPOt9/bJG4PTq6lQ6ecG6GzaOuOy/ynG5+yRiFlNs="},"cipher":{"name":"nacl/secretbox","nonce":"aw1ng1jHaDz/tQ7V2gR9O2+IGQ8xJEuE"},"ciphertext":"HycvuLZL4sYH0BrYTh4E/H20VtAW6u5zL5Pr+IBjYLYnCPzDkq8="}`), + OWASP: []byte(`{"kdf":{"name":"scrypt","params":{"N":131072,"r":8,"p":1},"salt":"m38E3kouJTtiheLQN22NQ8DTito5hrjpUIskqcd375k="},"cipher":{"name":"nacl/secretbox","nonce":"Y6PM13yA+o44pE/W1ZBwczeGnTV/m9Zc"},"ciphertext":"6H8sqj1K6B6yDjtH5AQ6lbFigg/C2yDDJc4rYJ79w9aVPImFIPI="}`), + } +) + // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { TestingT(t) } @@ -61,3 +70,89 @@ func (EncryptedSuite) TestDecrypt(c *C) { c.Assert(err, IsNil) c.Assert(dec, DeepEquals, plaintext) } + +func (EncryptedSuite) TestMarshalUnmarshal(c *C) { + passphrase := []byte("supersecret") + + wrapped, err := Marshal(plaintext, passphrase) + c.Assert(err, IsNil) + c.Assert(wrapped, NotNil) + + var protected []byte + err = Unmarshal(wrapped, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) +} + +func (EncryptedSuite) TestInvalidKDFSettings(c *C) { + passphrase := []byte("supersecret") + + wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, 0) + c.Assert(err, IsNil) + c.Assert(wrapped, NotNil) + + var protected []byte + err = Unmarshal(wrapped, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) +} + +func (EncryptedSuite) TestLegacyKDFSettings(c *C) { + passphrase := []byte("supersecret") + + wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, Legacy) + c.Assert(err, IsNil) + c.Assert(wrapped, NotNil) + + var protected []byte + err = Unmarshal(wrapped, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) +} + +func (EncryptedSuite) TestStandardKDFSettings(c *C) { + passphrase := []byte("supersecret") + + wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, Standard) + c.Assert(err, IsNil) + c.Assert(wrapped, NotNil) + + var protected []byte + err = Unmarshal(wrapped, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) +} + +func (EncryptedSuite) TestOWASPKDFSettings(c *C) { + passphrase := []byte("supersecret") + + wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, OWASP) + c.Assert(err, IsNil) + c.Assert(wrapped, NotNil) + + var protected []byte + err = Unmarshal(wrapped, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) +} + +func (EncryptedSuite) TestKDFSettingVectors(c *C) { + passphrase := []byte("supersecret") + + for _, v := range kdfVectors { + var protected []byte + err := Unmarshal(v, &protected, passphrase) + c.Assert(err, IsNil) + c.Assert(protected, DeepEquals, plaintext) + } +} + +func (EncryptedSuite) TestUnsupportedKDFParameters(c *C) { + enc := []byte(`{"kdf":{"name":"scrypt","params":{"N":99,"r":99,"p":99},"salt":"cZFcQJdwPhPyhU1R4qkl0qVOIjZd4V/7LYYAavq166k="},"cipher":{"name":"nacl/secretbox","nonce":"7vhRS7j0hEPBWV05skAdgLj81AkGeE7U"},"ciphertext":"6WYU/YSXVbYzl/NzaeAzmjLyfFhOOjLc0d8/GFV0aBFdJvyCcXc="}`) + passphrase := []byte("supersecret") + + dec, err := Decrypt(enc, passphrase) + c.Assert(err, NotNil) + c.Assert(dec, IsNil) + c.Assert(strings.Contains(err.Error(), "unsupported scrypt parameters"), Equals, true) +}