From f0d7f3f1bd422255accb1c0ffcc21baaa7552355 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Aug 2016 12:32:38 +0200 Subject: [PATCH] Calibrate scrypt for the current hardware Closes #17 --- src/restic/crypto/kdf.go | 74 +++++++++++++++++++++++++++++++++-- src/restic/crypto/kdf_test.go | 14 +++++++ src/restic/repository/key.go | 52 +++++++++++++++--------- 3 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 src/restic/crypto/kdf_test.go diff --git a/src/restic/crypto/kdf.go b/src/restic/crypto/kdf.go index 3551fff60..7fe124e44 100644 --- a/src/restic/crypto/kdf.go +++ b/src/restic/crypto/kdf.go @@ -1,22 +1,76 @@ package crypto import ( + "crypto/rand" "fmt" + "time" + sscrypt "github.com/elithrar/simple-scrypt" "golang.org/x/crypto/scrypt" ) +const saltLength = 64 + +// KDFParams are the default parameters used for the key derivation function KDF(). +type KDFParams struct { + N int + R int + P int +} + +// DefaultKDFParams are the default parameters used for Calibrate and KDF(). +var DefaultKDFParams = KDFParams{ + N: sscrypt.DefaultParams.N, + R: sscrypt.DefaultParams.R, + P: sscrypt.DefaultParams.P, +} + +// Calibrate determines new KDF parameters for the current hardware. +func Calibrate(timeout time.Duration, memory int) (KDFParams, error) { + defaultParams := sscrypt.Params{ + N: DefaultKDFParams.N, + R: DefaultKDFParams.R, + P: DefaultKDFParams.P, + DKLen: sscrypt.DefaultParams.DKLen, + SaltLen: sscrypt.DefaultParams.SaltLen, + } + + params, err := sscrypt.Calibrate(timeout, memory, defaultParams) + if err != nil { + return DefaultKDFParams, err + } + + return KDFParams{ + N: params.N, + R: params.R, + P: params.P, + }, nil +} + // KDF derives encryption and message authentication keys from the password // using the supplied parameters N, R and P and the Salt. -func KDF(N, R, P int, salt []byte, password string) (*Key, error) { - if len(salt) == 0 { - return nil, fmt.Errorf("scrypt() called with empty salt") +func KDF(p KDFParams, salt []byte, password string) (*Key, error) { + if len(salt) != saltLength { + return nil, fmt.Errorf("scrypt() called with invalid salt bytes (len %d)", len(salt)) + } + + // make sure we have valid parameters + params := sscrypt.Params{ + N: p.N, + R: p.R, + P: p.P, + DKLen: sscrypt.DefaultParams.DKLen, + SaltLen: len(salt), + } + + if err := params.Check(); err != nil { + return nil, err } derKeys := &Key{} keybytes := macKeySize + aesKeySize - scryptKeys, err := scrypt.Key([]byte(password), salt, N, R, P, keybytes) + scryptKeys, err := scrypt.Key([]byte(password), salt, p.N, p.R, p.P, keybytes) if err != nil { return nil, fmt.Errorf("error deriving keys from password: %v", err) } @@ -33,3 +87,15 @@ func KDF(N, R, P int, salt []byte, password string) (*Key, error) { return derKeys, nil } + +// NewSalt returns new random salt bytes to use with KDF(). If NewSalt returns +// an error, this is a grave situation and the program must abort and terminate. +func NewSalt() ([]byte, error) { + buf := make([]byte, saltLength) + n, err := rand.Read(buf) + if n != saltLength || err != nil { + panic("unable to read enough random bytes for new salt") + } + + return buf, nil +} diff --git a/src/restic/crypto/kdf_test.go b/src/restic/crypto/kdf_test.go new file mode 100644 index 000000000..5823eb889 --- /dev/null +++ b/src/restic/crypto/kdf_test.go @@ -0,0 +1,14 @@ +package crypto + +import ( + "testing" + "time" +) + +func TestCalibrate(t *testing.T) { + params, err := Calibrate(100*time.Millisecond, 50) + if err != nil { + t.Fatal(err) + } + t.Logf("testing calibrate, params after: %v", params) +} diff --git a/src/restic/repository/key.go b/src/restic/repository/key.go index 0a3dfe58e..417ef615f 100644 --- a/src/restic/repository/key.go +++ b/src/restic/repository/key.go @@ -1,7 +1,6 @@ package repository import ( - "crypto/rand" "encoding/json" "errors" "fmt" @@ -19,15 +18,6 @@ var ( ErrNoKeyFound = errors.New("wrong password or no key found") ) -// TODO: figure out scrypt values on the fly depending on the current -// hardware. -const ( - scryptN = 65536 - scryptR = 8 - scryptP = 1 - scryptSaltsize = 64 -) - // Key represents an encrypted master key for a repository. type Key struct { Created time.Time `json:"created"` @@ -47,6 +37,15 @@ type Key struct { name string } +// KDFParams tracks the parameters used for the KDF. If not set, it will be +// calibrated on the first run of AddKey(). +var KDFParams *crypto.KDFParams + +var ( + KDFTimeout = 500 * time.Millisecond // timeout for KDF + KDFMemory = 60 // max memory for KDF, in MiB +) + // createMasterKey creates a new master key in the given backend and encrypts // it with the password. func createMasterKey(s *Repository, password string) (*Key, error) { @@ -67,7 +66,12 @@ func OpenKey(s *Repository, name string, password string) (*Key, error) { } // derive user key - k.user, err = crypto.KDF(k.N, k.R, k.P, k.Salt, password) + params := crypto.KDFParams{ + N: k.N, + R: k.R, + P: k.P, + } + k.user, err = crypto.KDF(params, k.Salt, password) if err != nil { return nil, err } @@ -134,13 +138,24 @@ func LoadKey(s *Repository, name string) (k *Key, err error) { // AddKey adds a new key to an already existing repository. func AddKey(s *Repository, password string, template *crypto.Key) (*Key, error) { + // make sure we have valid KDF parameters + if KDFParams == nil { + p, err := crypto.Calibrate(KDFTimeout, KDFMemory) + if err != nil { + return nil, err + } + + KDFParams = &p + debug.Log("repository.AddKey", "calibrated KDF parameters are %v", p) + } + // fill meta data about key newkey := &Key{ Created: time.Now(), KDF: "scrypt", - N: scryptN, - R: scryptR, - P: scryptP, + N: KDFParams.N, + R: KDFParams.R, + P: KDFParams.P, } hn, err := os.Hostname() @@ -154,14 +169,13 @@ func AddKey(s *Repository, password string, template *crypto.Key) (*Key, error) } // generate random salt - newkey.Salt = make([]byte, scryptSaltsize) - n, err := rand.Read(newkey.Salt) - if n != scryptSaltsize || err != nil { - panic("unable to read enough random bytes for salt") + newkey.Salt, err = crypto.NewSalt() + if err != nil { + panic("unable to read enough random bytes for salt: " + err.Error()) } // call KDF to derive user key - newkey.user, err = crypto.KDF(newkey.N, newkey.R, newkey.P, newkey.Salt, password) + newkey.user, err = crypto.KDF(*KDFParams, newkey.Salt, password) if err != nil { return nil, err }