Skip to content

Commit

Permalink
feat: increase scrypt parameters (#470)
Browse files Browse the repository at this point in the history
* feat(encrypted): adaptative scrypt parameters

Signed-off-by: Thibault Normand <me@zenithar.org>

* feat(encrypted): ensure standard parameters by default

Signed-off-by: Thibault Normand <me@zenithar.org>

* test(encrypted): add vector tests

Signed-off-by: Thibault Normand <me@zenithar.org>

---------

Signed-off-by: Thibault Normand <me@zenithar.org>
Co-authored-by: Radoslav Dimitrov <dimitrovr@vmware.com>
  • Loading branch information
Zenithar and rdimitrov committed Jul 5, 2023
1 parent 6adc195 commit 6aa3072
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 19 deletions.
102 changes: 83 additions & 19 deletions encrypted/encrypted.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
95 changes: 95 additions & 0 deletions encrypted/encrypted_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down Expand Up @@ -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)
}

0 comments on commit 6aa3072

Please sign in to comment.