Merge pull request #577 from restic/dynamic-scrypt

Dynamically calibrate scrypt parameters
This commit is contained in:
Alexander Neumann 2016-08-21 15:00:24 +02:00
commit 11d01fcd32
19 changed files with 837 additions and 418 deletions

View File

@ -197,6 +197,8 @@ func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) string {
return pw1
}
const maxKeys = 20
// OpenRepository reads the password and opens the repository.
func (o GlobalOptions) OpenRepository() (*repository.Repository, error) {
if o.Repo == "" {
@ -214,7 +216,7 @@ func (o GlobalOptions) OpenRepository() (*repository.Repository, error) {
o.password = o.ReadPassword("enter password for repository: ")
}
err = s.SearchKey(o.password)
err = s.SearchKey(o.password, maxKeys)
if err != nil {
return nil, fmt.Errorf("unable to open repo: %v", err)
}

View File

@ -40,6 +40,8 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) backend.IDs {
}
func cmdInit(t testing.TB, global GlobalOptions) {
repository.TestUseLowSecurityKDFParameters(t)
cmd := &CmdInit{global: &global}
OK(t, cmd.Execute(nil))

View File

@ -239,6 +239,8 @@ func induceError(data []byte) {
func TestCheckerModifiedData(t *testing.T) {
be := mem.New()
repository.TestUseLowSecurityKDFParameters(t)
repo := repository.New(be)
OK(t, repo.Init(TestPassword))
@ -249,7 +251,7 @@ func TestCheckerModifiedData(t *testing.T) {
beError := &errorBackend{Backend: be}
checkRepo := repository.New(beError)
OK(t, checkRepo.SearchKey(TestPassword))
OK(t, checkRepo.SearchKey(TestPassword, 5))
chkr := checker.New(checkRepo)

View File

@ -9,7 +9,6 @@ import (
"fmt"
"golang.org/x/crypto/poly1305"
"golang.org/x/crypto/scrypt"
)
const (
@ -315,34 +314,6 @@ func Decrypt(ks *Key, plaintext []byte, ciphertextWithMac []byte) ([]byte, error
return plaintext, 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")
}
derKeys := &Key{}
keybytes := macKeySize + aesKeySize
scryptKeys, err := scrypt.Key([]byte(password), salt, N, R, P, keybytes)
if err != nil {
return nil, fmt.Errorf("error deriving keys from password: %v", err)
}
if len(scryptKeys) != keybytes {
return nil, fmt.Errorf("invalid numbers of bytes expanded from scrypt(): %d", len(scryptKeys))
}
// first 32 byte of scrypt output is the encryption key
copy(derKeys.Encrypt[:], scryptKeys[:aesKeySize])
// next 32 byte of scrypt output is the mac key, in the form k||r
macKeyFromSlice(&derKeys.MAC, scryptKeys[aesKeySize:])
return derKeys, nil
}
// Valid tests if the key is valid.
func (k *Key) Valid() bool {
return k.Encrypt.Valid() && k.MAC.Valid()

View File

@ -4,12 +4,12 @@ import (
"bytes"
"crypto/rand"
"io"
"io/ioutil"
"testing"
"github.com/restic/chunker"
"restic/crypto"
. "restic/test"
"github.com/restic/chunker"
)
const testLargeCrypto = false
@ -128,25 +128,6 @@ func TestLargeEncrypt(t *testing.T) {
}
}
func BenchmarkEncryptWriter(b *testing.B) {
size := 8 << 20 // 8MiB
k := crypto.NewRandomKey()
b.ResetTimer()
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
rd := RandomLimitReader(23, size)
wr := crypto.EncryptTo(k, ioutil.Discard)
n, err := io.Copy(wr, rd)
OK(b, err)
OK(b, wr.Close())
Assert(b, n == int64(size),
"not enough bytes writter: want %d, got %d", size, n)
}
}
func BenchmarkEncrypt(b *testing.B) {
size := 8 << 20 // 8MiB
data := make([]byte, size)
@ -163,55 +144,6 @@ func BenchmarkEncrypt(b *testing.B) {
}
}
func BenchmarkDecryptReader(b *testing.B) {
size := 8 << 20 // 8MiB
buf := Random(23, size)
k := crypto.NewRandomKey()
ciphertext := make([]byte, len(buf)+crypto.Extension)
_, err := crypto.Encrypt(k, ciphertext, buf)
OK(b, err)
rd := bytes.NewReader(ciphertext)
b.ResetTimer()
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
rd.Seek(0, 0)
decRd, err := crypto.DecryptFrom(k, rd)
OK(b, err)
_, err = io.Copy(ioutil.Discard, decRd)
OK(b, err)
}
}
func BenchmarkEncryptDecryptReader(b *testing.B) {
k := crypto.NewRandomKey()
size := 8 << 20 // 8MiB
b.ResetTimer()
b.SetBytes(int64(size))
buf := bytes.NewBuffer(nil)
for i := 0; i < b.N; i++ {
rd := RandomLimitReader(23, size)
buf.Reset()
wr := crypto.EncryptTo(k, buf)
_, err := io.Copy(wr, rd)
OK(b, err)
OK(b, wr.Close())
r, err := crypto.DecryptFrom(k, buf)
OK(b, err)
_, err = io.Copy(ioutil.Discard, r)
OK(b, err)
}
}
func BenchmarkDecrypt(b *testing.B) {
size := 8 << 20 // 8MiB
data := make([]byte, size)
@ -232,101 +164,3 @@ func BenchmarkDecrypt(b *testing.B) {
OK(b, err)
}
}
func TestEncryptStreamWriter(t *testing.T) {
k := crypto.NewRandomKey()
tests := []int{5, 23, 2<<18 + 23, 1 << 20}
if testLargeCrypto {
tests = append(tests, 7<<20+123)
}
for _, size := range tests {
data := Random(42, size)
ciphertext := bytes.NewBuffer(nil)
wr := crypto.EncryptTo(k, ciphertext)
_, err := io.Copy(wr, bytes.NewReader(data))
OK(t, err)
OK(t, wr.Close())
l := len(data) + crypto.Extension
Assert(t, len(ciphertext.Bytes()) == l,
"wrong ciphertext length: expected %d, got %d",
l, len(ciphertext.Bytes()))
// decrypt with default function
plaintext, err := crypto.Decrypt(k, []byte{}, ciphertext.Bytes())
OK(t, err)
Assert(t, bytes.Equal(data, plaintext),
"wrong plaintext after decryption: expected %02x, got %02x",
data, plaintext)
}
}
func TestDecryptStreamReader(t *testing.T) {
k := crypto.NewRandomKey()
tests := []int{5, 23, 2<<18 + 23, 1 << 20}
if testLargeCrypto {
tests = append(tests, 7<<20+123)
}
for _, size := range tests {
data := Random(42, size)
var err error
ciphertext := make([]byte, size+crypto.Extension)
// encrypt with default function
ciphertext, err = crypto.Encrypt(k, ciphertext, data)
OK(t, err)
Assert(t, len(ciphertext) == len(data)+crypto.Extension,
"wrong number of bytes returned after encryption: expected %d, got %d",
len(data)+crypto.Extension, len(ciphertext))
rd, err := crypto.DecryptFrom(k, bytes.NewReader(ciphertext))
OK(t, err)
plaintext, err := ioutil.ReadAll(rd)
OK(t, err)
Assert(t, bytes.Equal(data, plaintext),
"wrong plaintext after decryption: expected %02x, got %02x",
data, plaintext)
}
}
func TestEncryptWriter(t *testing.T) {
k := crypto.NewRandomKey()
tests := []int{5, 23, 2<<18 + 23, 1 << 20}
if testLargeCrypto {
tests = append(tests, 7<<20+123)
}
for _, size := range tests {
data := Random(42, size)
buf := bytes.NewBuffer(nil)
wr := crypto.EncryptTo(k, buf)
_, err := io.Copy(wr, bytes.NewReader(data))
OK(t, err)
OK(t, wr.Close())
ciphertext := buf.Bytes()
l := len(data) + crypto.Extension
Assert(t, len(ciphertext) == l,
"wrong ciphertext length: expected %d, got %d",
l, len(ciphertext))
// decrypt with default function
plaintext, err := crypto.Decrypt(k, []byte{}, ciphertext)
OK(t, err)
Assert(t, bytes.Equal(data, plaintext),
"wrong plaintext after decryption: expected %02x, got %02x",
data, plaintext)
}
}

101
src/restic/crypto/kdf.go Normal file
View File

@ -0,0 +1,101 @@
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(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, p.N, p.R, p.P, keybytes)
if err != nil {
return nil, fmt.Errorf("error deriving keys from password: %v", err)
}
if len(scryptKeys) != keybytes {
return nil, fmt.Errorf("invalid numbers of bytes expanded from scrypt(): %d", len(scryptKeys))
}
// first 32 byte of scrypt output is the encryption key
copy(derKeys.Encrypt[:], scryptKeys[:aesKeySize])
// next 32 byte of scrypt output is the mac key, in the form k||r
macKeyFromSlice(&derKeys.MAC, scryptKeys[aesKeySize:])
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
}

View File

@ -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)
}

View File

@ -1,87 +0,0 @@
package crypto
import (
"bytes"
"errors"
"io"
)
type decryptReader struct {
buf []byte
rd *bytes.Reader
}
func (d *decryptReader) Read(dst []byte) (n int, err error) {
if d.buf == nil {
return 0, io.EOF
}
n, err = d.rd.Read(dst)
if err == io.EOF {
d.free()
}
return
}
func (d *decryptReader) free() {
freeBuffer(d.buf)
d.buf = nil
}
func (d *decryptReader) Close() error {
if d == nil || d.buf == nil {
return nil
}
d.free()
return nil
}
func (d *decryptReader) ReadByte() (c byte, err error) {
if d.buf == nil {
return 0, io.EOF
}
c, err = d.rd.ReadByte()
if err == io.EOF {
d.free()
}
return
}
func (d *decryptReader) WriteTo(w io.Writer) (n int64, err error) {
if d.buf == nil {
return 0, errors.New("WriteTo() called on drained reader")
}
n, err = d.rd.WriteTo(w)
d.free()
return
}
// DecryptFrom verifies and decrypts the ciphertext read from rd with ks and
// makes it available on the returned Reader. Ciphertext must be in the form IV
// || Ciphertext || MAC. In order to correctly verify the ciphertext, rd is
// drained, locally buffered and made available on the returned Reader
// afterwards. If a MAC verification failure is observed, it is returned
// immediately.
func DecryptFrom(ks *Key, rd io.Reader) (io.ReadCloser, error) {
buf := bytes.NewBuffer(getBuffer()[:0])
_, err := buf.ReadFrom(rd)
if err != nil {
return (*decryptReader)(nil), err
}
ciphertext := buf.Bytes()
ciphertext, err = Decrypt(ks, ciphertext, ciphertext)
if err != nil {
freeBuffer(ciphertext)
return (*decryptReader)(nil), err
}
return &decryptReader{buf: ciphertext, rd: bytes.NewReader(ciphertext)}, nil
}

View File

@ -1,88 +0,0 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"io"
)
type encryptWriter struct {
data []byte
key *Key
s cipher.Stream
w io.Writer
closed bool
}
func (e *encryptWriter) Close() error {
if e == nil {
return nil
}
if e.closed {
return errors.New("Close() called on already closed writer")
}
e.closed = true
// encrypt everything
iv, c := e.data[:ivSize], e.data[ivSize:]
e.s.XORKeyStream(c, c)
// compute mac
mac := poly1305MAC(c, iv, &e.key.MAC)
e.data = append(e.data, mac...)
// write everything
n, err := e.w.Write(e.data)
if err != nil {
return err
}
if n != len(e.data) {
return errors.New("not all bytes written")
}
// return buffer to pool
freeBuffer(e.data)
return nil
}
func (e *encryptWriter) Write(p []byte) (int, error) {
// if e.data is too small, return it to the buffer and create new slice
if cap(e.data) < len(e.data)+len(p) {
b := make([]byte, len(e.data), len(e.data)*2)
copy(b, e.data)
freeBuffer(e.data)
e.data = b
}
// copy new data to e.data
e.data = append(e.data, p...)
return len(p), nil
}
// EncryptTo buffers data written to the returned io.WriteCloser. When Close()
// is called, the data is encrypted and written to the underlying writer.
func EncryptTo(ks *Key, wr io.Writer) io.WriteCloser {
ew := &encryptWriter{
data: getBuffer(),
key: ks,
}
// buffer iv for mac
ew.data = ew.data[:ivSize]
copy(ew.data, newIV())
c, err := aes.NewCipher(ks.Encrypt[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
ew.s = cipher.NewCTR(c, ew.data[:ivSize])
ew.w = wr
return ew
}

View File

@ -4,7 +4,6 @@ import (
"math/rand"
"restic"
"restic/backend"
"restic/backend/local"
"restic/pack"
"restic/repository"
. "restic/test"
@ -124,21 +123,6 @@ func TestIndexLoad(t *testing.T) {
}
}
func openRepo(t testing.TB, dir, password string) *repository.Repository {
b, err := local.Open(dir)
if err != nil {
t.Fatalf("open backend %v failed: %v", dir, err)
}
r := repository.New(b)
err = r.SearchKey(password)
if err != nil {
t.Fatalf("unable to open repo with password: %v", err)
}
return r
}
func BenchmarkIndexNew(b *testing.B) {
repo, cleanup := createFilledRepo(b, 3, 0)
defer cleanup()

View File

@ -1,7 +1,6 @@
package repository
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
@ -17,15 +16,9 @@ import (
var (
// ErrNoKeyFound is returned when no key for the repository could be decrypted.
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
// ErrMaxKeysReached is returned when the maximum number of keys was checked and no key could be found.
ErrMaxKeysReached = errors.New("maximum number of keys reached")
)
// Key represents an encrypted master key for a repository.
@ -47,6 +40,18 @@ 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 specifies the maximum runtime for the KDF.
KDFTimeout = 500 * time.Millisecond
// KDFMemory limits the memory the KDF is allowed to use.
KDFMemory = 60
)
// 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 +72,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
}
@ -94,18 +104,32 @@ func OpenKey(s *Repository, name string, password string) (*Key, error) {
return k, nil
}
// SearchKey tries to decrypt all keys in the backend with the given password.
// If none could be found, ErrNoKeyFound is returned.
func SearchKey(s *Repository, password string) (*Key, error) {
// try all keys in repo
// SearchKey tries to decrypt at most maxKeys keys in the backend with the
// given password. If none could be found, ErrNoKeyFound is returned. When
// maxKeys is reached, ErrMaxKeysReached is returned. When setting maxKeys to
// zero, all keys in the repo are checked.
func SearchKey(s *Repository, password string, maxKeys int) (*Key, error) {
checked := 0
// try at most maxKeysForSearch keys in repo
done := make(chan struct{})
defer close(done)
for name := range s.Backend().List(backend.Key, done) {
if maxKeys > 0 && checked > maxKeys {
return nil, ErrMaxKeysReached
}
debug.Log("SearchKey", "trying key %v", name[:12])
key, err := OpenKey(s, name, password)
if err != nil {
debug.Log("SearchKey", "key %v returned error %v", name[:12], err)
continue
// ErrUnauthenticated means the password is wrong, try the next key
if err == crypto.ErrUnauthenticated {
continue
}
return nil, err
}
debug.Log("SearchKey", "successfully opened key %v", name[:12])
@ -134,13 +158,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 +189,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
}

View File

@ -405,9 +405,9 @@ func LoadIndex(repo *Repository, id backend.ID) (*Index, error) {
}
// SearchKey finds a key with the supplied password, afterwards the config is
// read and parsed.
func (r *Repository) SearchKey(password string) error {
key, err := SearchKey(r, password)
// read and parsed. It tries at most maxKeys key files in the repo.
func (r *Repository) SearchKey(password string, maxKeys int) error {
key, err := SearchKey(r, password, maxKeys)
if err != nil {
return err
}

View File

@ -5,11 +5,25 @@ import (
"restic/backend"
"restic/backend/local"
"restic/backend/mem"
"restic/crypto"
"testing"
"github.com/restic/chunker"
)
// testKDFParams are the parameters for the KDF to be used during testing.
var testKDFParams = crypto.KDFParams{
N: 128,
R: 1,
P: 1,
}
// TestUseLowSecurityKDFParameters configures low-security KDF parameters for testing.
func TestUseLowSecurityKDFParameters(t testing.TB) {
t.Logf("using low-security KDF parameters for test")
KDFParams = &testKDFParams
}
// TestBackend returns a fully configured in-memory backend.
func TestBackend(t testing.TB) (be backend.Backend, cleanup func()) {
return mem.New(), func() {}
@ -22,8 +36,10 @@ const testChunkerPol = chunker.Pol(0x3DA3358B4DC173)
// TestRepositoryWithBackend returns a repository initialized with a test
// password. If be is nil, an in-memory backend is used. A constant polynomial
// is used for the chunker.
// is used for the chunker and low-security test parameters.
func TestRepositoryWithBackend(t testing.TB, be backend.Backend) (r *Repository, cleanup func()) {
TestUseLowSecurityKDFParameters(t)
var beCleanup func()
if be == nil {
be, beCleanup = TestBackend(t)

View File

@ -214,7 +214,7 @@ func OpenLocalRepo(t testing.TB, dir string) *repository.Repository {
OK(t, err)
repo := repository.New(be)
err = repo.SearchKey(TestPassword)
err = repo.SearchKey(TestPassword, 10)
OK(t, err)
return repo

6
vendor/manifest vendored
View File

@ -7,6 +7,12 @@
"revision": "18419ee53958df28fcfc9490fe6123bd59e237bb",
"branch": "HEAD"
},
{
"importpath": "github.com/elithrar/simple-scrypt",
"repository": "https://github.com/elithrar/simple-scrypt",
"revision": "cbb1ebac08e2ca5495a43f4ef5555e61a7ec7677",
"branch": "master"
},
{
"importpath": "github.com/jessevdk/go-flags",
"repository": "https://github.com/jessevdk/go-flags",

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Matthew Silverlock (matt@eatsleeprepeat.net)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,155 @@
# simple-scrypt
[![GoDoc](https://godoc.org/github.com/elithrar/simple-scrypt?status.svg)](https://godoc.org/github.com/elithrar/simple-scrypt) [![Build Status](https://travis-ci.org/elithrar/simple-scrypt.svg?branch=master)](https://travis-ci.org/elithrar/simple-scrypt)
simple-scrypt provides a convenience wrapper around Go's existing
[scrypt](http://golang.org/x/crypto/scrypt) package that makes it easier to
securely derive strong keys ("hash user passwords"). This library allows you to:
* Generate a scrypt derived key with a crytographically secure salt and sane
default parameters for N, r and p.
* Upgrade the parameters used to generate keys as hardware improves by storing
them with the derived key (the scrypt spec. doesn't allow for this by
default).
* Provide your own parameters (if you wish to).
The API closely mirrors Go's [bcrypt](https://golang.org/x/crypto/bcrypt)
library in an effort to make it easy to migrate—and because it's an easy to grok
API.
## Installation
With a [working Go toolchain](https://golang.org/doc/code.html):
```sh
go get -u github.com/elithrar/simple-scrypt
```
## Example
simple-scrypt doesn't try to re-invent the wheel or do anything "special". It
wraps the `scrypt.Key` function as thinly as possible, generates a
crytographically secure salt for you using Go's `crypto/rand` package, and
returns the derived key with the parameters prepended:
```go
package main
import(
"fmt"
"log"
"github.com/elithrar/simple-scrypt"
)
func main() {
// e.g. r.PostFormValue("password")
passwordFromForm := "prew8fid9hick6c"
// Generates a derived key of the form "N$r$p$salt$dk" where N, r and p are defined as per
// Colin Percival's scrypt paper: http://www.tarsnap.com/scrypt/scrypt.pdf
// scrypt.Defaults (N=16384, r=8, p=1) makes it easy to provide these parameters, and
// (should you wish) provide your own values via the scrypt.Params type.
hash, err := scrypt.GenerateFromPassword([]byte(passwordFromForm), scrypt.DefaultParams)
if err != nil {
log.Fatal(err)
}
// Print the derived key with its parameters prepended.
fmt.Printf("%s\n", hash)
// Uses the parameters from the existing derived key. Return an error if they don't match.
err := scrypt.CompareHashAndPassword(hash, []byte(passwordFromForm))
if err != nil {
log.Fatal(err)
}
}
```
## Upgrading Parameters
Upgrading derived keys from a set of parameters to a "stronger" set of parameters
as hardware improves, or as you scale (and move your auth process to separate
hardware), can be pretty useful. Here's how to do it with simple-scrypt:
```go
func main() {
// SCENE: We've successfully authenticated a user, compared their submitted
// (cleartext) password against the derived key stored in our database, and
// now want to upgrade the parameters (more rounds, more parallelism) to
// reflect some shiny new hardware we just purchased. As the user is logging
// in, we can retrieve the parameters used to generate their key, and if
// they don't match our "new" parameters, we can re-generate the key while
// we still have the cleartext password in memory
// (e.g. before the HTTP request ends).
current, err := scrypt.Cost(hash)
if err != nil {
log.Fatal(err)
}
// Now to check them against our own Params struct (e.g. using reflect.DeepEquals)
// and determine whether we want to generate a new key with our "upgraded" parameters.
slower := scrypt.Params{
N: 32768,
R: 8,
P: 2,
SaltLen: 16,
DKLen: 32,
}
if !reflect.DeepEqual(current, slower) {
// Re-generate the key with the slower parameters
// here using scrypt.GenerateFromPassword
}
}
```
## Automatically Determining Parameters
Thanks to the work by [tgulacsi](https://github.com/tgulacsi), you can have simple-scrypt
automatically determine the optimal parameters for you (time vs. memory). You should run this once
on program startup, as calibrating parameters can be an expensive operation.
```go
var params scrypt.Params
func main() {
var err error
// 500ms, 64MB of RAM per hash.
params, err = scrypt.Calibrate(500*time.Millisecond, 64, Params{})
if err != nil {
return nil, err
}
...
}
func RegisterUserHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Make sure you validate: not empty, not too long, etc.
email := r.PostFormValue("email")
pass := r.PostFormValue("password")
// Use our calibrated parameters
hash, err := scrypt.GenerateFromPassword([]byte(pass), params)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Save to DB, etc.
}
```
Be aware that increasing these, whilst making it harder to brute-force the resulting hash, also
increases the risk of a denial-of-service attack against your server. A surge in authenticate
attempts (even if legitimate!) could consume all available resources.
## License
MIT Licensed. See LICENSE file for details.

View File

@ -0,0 +1,295 @@
// Package scrypt provides a convenience wrapper around Go's existing scrypt package
// that makes it easier to securely derive strong keys from weak
// inputs (i.e. user passwords).
// The package provides password generation, constant-time comparison and
// parameter upgrading for scrypt derived keys.
package scrypt
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
"golang.org/x/crypto/scrypt"
)
// Constants
const (
maxInt = 1<<31 - 1
minDKLen = 16 // the minimum derived key length in bytes.
minSaltLen = 8 // the minimum allowed salt length in bytes.
)
// Params describes the input parameters to the scrypt
// key derivation function as per Colin Percival's scrypt
// paper: http://www.tarsnap.com/scrypt/scrypt.pdf
type Params struct {
N int // CPU/memory cost parameter (logN)
R int // block size parameter (octets)
P int // parallelisation parameter (positive int)
SaltLen int // bytes to use as salt (octets)
DKLen int // length of the derived key (octets)
}
// DefaultParams provides sensible default inputs into the scrypt function
// for interactive use (i.e. web applications).
// These defaults will consume approxmiately 16MB of memory (128 * r * N).
// The default key length is 256 bits.
var DefaultParams = Params{N: 16384, R: 8, P: 1, SaltLen: 16, DKLen: 32}
// ErrInvalidHash is returned when failing to parse a provided scrypt
// hash and/or parameters.
var ErrInvalidHash = errors.New("scrypt: the provided hash is not in the correct format")
// ErrInvalidParams is returned when the cost parameters (N, r, p), salt length
// or derived key length are invalid.
var ErrInvalidParams = errors.New("scrypt: the parameters provided are invalid")
// ErrMismatchedHashAndPassword is returned when a password (hashed) and
// given hash do not match.
var ErrMismatchedHashAndPassword = errors.New("scrypt: the hashed password does not match the hash of the given password")
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// err == nil only if len(b) == n
if err != nil {
return nil, err
}
return b, nil
}
// GenerateFromPassword returns the derived key of the password using the
// parameters provided. The parameters are prepended to the derived key and
// separated by the "$" character (0x24).
// If the parameters provided are less than the minimum acceptable values,
// an error will be returned.
func GenerateFromPassword(password []byte, params Params) ([]byte, error) {
salt, err := GenerateRandomBytes(params.SaltLen)
if err != nil {
return nil, err
}
if err := params.Check(); err != nil {
return nil, err
}
// scrypt.Key returns the raw scrypt derived key.
dk, err := scrypt.Key(password, salt, params.N, params.R, params.P, params.DKLen)
if err != nil {
return nil, err
}
// Prepend the params and the salt to the derived key, each separated
// by a "$" character. The salt and the derived key are hex encoded.
return []byte(fmt.Sprintf("%d$%d$%d$%x$%x", params.N, params.R, params.P, salt, dk)), nil
}
// CompareHashAndPassword compares a derived key with the possible cleartext
// equivalent. The parameters used in the provided derived key are used.
// The comparison performed by this function is constant-time. It returns nil
// on success, and an error if the derived keys do not match.
func CompareHashAndPassword(hash []byte, password []byte) error {
// Decode existing hash, retrieve params and salt.
params, salt, dk, err := decodeHash(hash)
if err != nil {
return err
}
// scrypt the cleartext password with the same parameters and salt
other, err := scrypt.Key(password, salt, params.N, params.R, params.P, params.DKLen)
if err != nil {
return err
}
// Constant time comparison
if subtle.ConstantTimeCompare(dk, other) == 1 {
return nil
}
return ErrMismatchedHashAndPassword
}
// Check checks that the parameters are valid for input into the
// scrypt key derivation function.
func (p *Params) Check() error {
// Validate N
if p.N > maxInt || p.N <= 1 || p.N%2 != 0 {
return ErrInvalidParams
}
// Validate r
if p.R < 1 || p.R > maxInt {
return ErrInvalidParams
}
// Validate p
if p.P < 1 || p.P > maxInt {
return ErrInvalidParams
}
// Validate that r & p don't exceed 2^30 and that N, r, p values don't
// exceed the limits defined by the scrypt algorithm.
if uint64(p.R)*uint64(p.P) >= 1<<30 || p.R > maxInt/128/p.P || p.R > maxInt/256 || p.N > maxInt/128/p.R {
return ErrInvalidParams
}
// Validate the salt length
if p.SaltLen < minSaltLen || p.SaltLen > maxInt {
return ErrInvalidParams
}
// Validate the derived key length
if p.DKLen < minDKLen || p.DKLen > maxInt {
return ErrInvalidParams
}
return nil
}
// decodeHash extracts the parameters, salt and derived key from the
// provided hash. It returns an error if the hash format is invalid and/or
// the parameters are invalid.
func decodeHash(hash []byte) (Params, []byte, []byte, error) {
vals := strings.Split(string(hash), "$")
// P, N, R, salt, scrypt derived key
if len(vals) != 5 {
return Params{}, nil, nil, ErrInvalidHash
}
var params Params
var err error
params.N, err = strconv.Atoi(vals[0])
if err != nil {
return params, nil, nil, ErrInvalidHash
}
params.R, err = strconv.Atoi(vals[1])
if err != nil {
return params, nil, nil, ErrInvalidHash
}
params.P, err = strconv.Atoi(vals[2])
if err != nil {
return params, nil, nil, ErrInvalidHash
}
salt, err := hex.DecodeString(vals[3])
if err != nil {
return params, nil, nil, ErrInvalidHash
}
params.SaltLen = len(salt)
dk, err := hex.DecodeString(vals[4])
if err != nil {
return params, nil, nil, ErrInvalidHash
}
params.DKLen = len(dk)
if err := params.Check(); err != nil {
return params, nil, nil, err
}
return params, salt, dk, nil
}
// Cost returns the scrypt parameters used to generate the derived key. This
// allows a package user to increase the cost (in time & resources) used as
// computational performance increases over time.
func Cost(hash []byte) (Params, error) {
params, _, _, err := decodeHash(hash)
return params, err
}
// Calibrate returns the hardest parameters (not weaker than the given params),
// allowed by the given limits.
// The returned params will not use more memory than the given (MiB);
// will not take more time than the given timeout, but more than timeout/2.
//
//
// The default timeout (when the timeout arg is zero) is 200ms.
// The default memMiBytes (when memMiBytes is zero) is 16MiB.
// The default parameters (when params == Params{}) is DefaultParams.
func Calibrate(timeout time.Duration, memMiBytes int, params Params) (Params, error) {
p := params
if p.N == 0 || p.R == 0 || p.P == 0 || p.SaltLen == 0 || p.DKLen == 0 {
p = DefaultParams
} else if err := p.Check(); err != nil {
return p, err
}
if timeout == 0 {
timeout = 200 * time.Millisecond
}
if memMiBytes == 0 {
memMiBytes = 16
}
salt, err := GenerateRandomBytes(p.SaltLen)
if err != nil {
return p, err
}
password := []byte("weakpassword")
// First, we calculate the minimal required time.
start := time.Now()
if _, err := scrypt.Key(password, salt, p.N, p.R, p.P, p.DKLen); err != nil {
return p, err
}
dur := time.Since(start)
for dur < timeout && p.N < maxInt>>1 {
p.N <<= 1
}
// Memory usage is at least 128 * r * N, see
// http://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html
// or https://drupal.org/comment/4675994#comment-4675994
var again bool
memBytes := memMiBytes << 20
// If we'd use more memory then the allowed, we can tune the memory usage
for 128*p.R*p.N > memBytes {
if p.R > 1 {
// by lowering r
p.R--
} else if p.N > 16 {
again = true
p.N >>= 1
} else {
break
}
}
if !again {
return p, p.Check()
}
// We have to compensate the lowering of N, by increasing p.
for i := 0; i < 10 && p.P > 0; i++ {
start := time.Now()
if _, err := scrypt.Key(password, salt, p.N, p.R, p.P, p.DKLen); err != nil {
return p, err
}
dur := time.Since(start)
if dur < timeout/2 {
p.P = int(float64(p.P)*float64(timeout/dur) + 1)
} else if dur > timeout && p.P > 1 {
p.P--
} else {
break
}
}
return p, p.Check()
}

View File

@ -0,0 +1,156 @@
package scrypt
import (
"fmt"
"reflect"
"testing"
"time"
)
// Test cases
var (
testLengths = []int{1, 8, 16, 32, 100, 500, 2500}
password = "super-secret-password"
)
var testParams = []struct {
pass bool
params Params
}{
{true, Params{16384, 8, 1, 32, 64}},
{true, Params{16384, 8, 1, 16, 32}},
{true, Params{65536, 8, 1, 16, 64}},
{true, Params{1048576, 8, 2, 64, 128}},
{false, Params{-1, 8, 1, 16, 32}}, // invalid N
{false, Params{0, 8, 1, 16, 32}}, // invalid N
{false, Params{1 << 31, 8, 1, 16, 32}}, // invalid N
{false, Params{16384, 0, 12, 16, 32}}, // invalid R
{false, Params{16384, 8, 0, 16, 32}}, // invalid R > maxInt/128/P
{false, Params{16384, 1 << 24, 1, 16, 32}}, // invalid R > maxInt/256
{false, Params{1 << 31, 8, 0, 16, 32}}, // invalid p < 0
{false, Params{4096, 8, 1, 5, 32}}, // invalid SaltLen
{false, Params{4096, 8, 1, 16, 2}}, // invalid DKLen
}
var testHashes = []struct {
pass bool
hash string
}{
{false, "1$8$1$9003d0e8e69482843e6bd560c2c9cd94$1976f233124e0ee32bb2678eb1b0ed668eb66cff6fa43279d1e33f6e81af893b"}, // N too small
{false, "$9003d0e8e69482843e6bd560c2c9cd94$1976f233124e0ee32bb2678eb1b0ed668eb66cff6fa43279d1e33f6e81af893b"}, // too short
{false, "16384#8#1#18fbc325efa37402d27c3c2172900cbf$d4e5e1b9eedc1a6a14aad6624ab57b7b42ae75b9c9845fde32de765835f2aaf9"}, // incorrect separators
{false, "16384$nogood$1$18fbc325efa37402d27c3c2172900cbf$d4e5e1b9eedc1a6a14aad6624ab57b7b42ae75b9c9845fde32de765835f2aaf9"}, // invalid R
{false, "16384$8$abc1$18fbc325efa37402d27c3c2172900cbf$d4e5e1b9eedc1a6a14aad6624ab57b7b42ae75b9c9845fde32de765835f2aaf9"}, // invalid P
{false, "16384$8$1$Tk9QRQ==$d4e5e1b9eedc1a6a14aad6624ab57b7b42ae75b9c9845fde32de765835f2aaf9"}, // invalid salt (not hex)
{false, "16384$8$1$18fbc325efa37402d27c3c2172900cbf$42ae====/75b9c9845fde32de765835f2aaf9"}, // invalid dk (not hex)
}
func TestGenerateRandomBytes(t *testing.T) {
for _, v := range testLengths {
_, err := GenerateRandomBytes(v)
if err != nil {
t.Fatalf("failed to generate random bytes")
}
}
}
func TestGenerateFromPassword(t *testing.T) {
for _, v := range testParams {
_, err := GenerateFromPassword([]byte(password), v.params)
if err != nil && v.pass == true {
t.Fatalf("no error was returned when expected for params: %+v", v.params)
}
}
}
func TestCompareHashAndPassword(t *testing.T) {
hash, err := GenerateFromPassword([]byte(password), DefaultParams)
if err != nil {
t.Fatal(err)
}
if err := CompareHashAndPassword(hash, []byte(password)); err != nil {
t.Fatal(err)
}
if err := CompareHashAndPassword(hash, []byte("invalid-password")); err == nil {
t.Fatalf("mismatched passwords did not produce an error")
}
invalidHash := []byte("$166$$11$a2ad56a415af5")
if err := CompareHashAndPassword(invalidHash, []byte(password)); err == nil {
t.Fatalf("did not identify an invalid hash")
}
}
func TestCost(t *testing.T) {
hash, err := GenerateFromPassword([]byte(password), DefaultParams)
if err != nil {
t.Fatal(err)
}
params, err := Cost(hash)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(params, DefaultParams) {
t.Fatal("cost mismatch: parameters used did not match those retrieved")
}
}
func TestDecodeHash(t *testing.T) {
for _, v := range testHashes {
_, err := Cost([]byte(v.hash))
if err == nil && v.pass == false {
t.Fatal("invalid hash: did not correctly detect invalid password hash")
}
}
}
func TestCalibrate(t *testing.T) {
timeout := 500 * time.Millisecond
for testNum, tc := range []struct {
MemMiB int
}{
{64},
{32},
{16},
{8},
{1},
} {
var (
p Params
err error
)
p, err = Calibrate(timeout, tc.MemMiB, p)
if err != nil {
t.Fatalf("%d. %#v: %v", testNum, p, err)
}
if (128*p.R*p.N)>>20 > tc.MemMiB {
t.Errorf("%d. wanted memory limit %d, got %d.", testNum, tc.MemMiB, (128*p.R*p.N)>>20)
}
start := time.Now()
_, err = GenerateFromPassword([]byte(password), p)
dur := time.Since(start)
t.Logf("GenerateFromPassword with %#v took %s (%v)", p, dur, err)
if err != nil {
t.Fatalf("%d. GenerateFromPassword with %#v: %v", testNum, p, err)
}
if dur < timeout/2 {
t.Errorf("%d. GenerateFromPassword was too fast (wanted around %s, got %s) with %#v.", testNum, timeout, dur, p)
} else if timeout*2 < dur {
t.Errorf("%d. GenerateFromPassword took too long (wanted around %s, got %s) with %#v.", testNum, timeout, dur, p)
}
}
}
func ExampleCalibrate() {
p, err := Calibrate(1*time.Second, 128, Params{})
if err != nil {
panic(err)
}
dk, err := GenerateFromPassword([]byte("super-secret-password"), p)
fmt.Printf("generated password is %q (%v)", dk, err)
}