diff options
author | Matt Silverlock <matt@eatsleeprepeat.net> | 2017-02-22 05:13:50 +0300 |
---|---|---|
committer | Matt Silverlock <matt@eatsleeprepeat.net> | 2017-02-22 05:13:50 +0300 |
commit | fbc587a6f03fd73ab27cc614367a1d3d0244ee90 (patch) | |
tree | a90bd4512b922910249a0393a34b7e6fbb0c719e | |
parent | 55fb70d4b7445f35318cdde4f62437408566708e (diff) |
[wip] first v2 cut.elithrar/v2
- Move to using crypto/nacl/secretbox for enc/dec
- Restructure how options are handled
- TODO: review serialization interface
- TODO: refactor multicodec support for rotated keys
-rw-r--r-- | errors.go | 1 | ||||
-rw-r--r-- | securecookie.go | 234 | ||||
-rw-r--r-- | serialize.go | 64 |
3 files changed, 175 insertions, 124 deletions
@@ -71,6 +71,7 @@ var ( errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errInvalidKey = cookieError{typ: usageError, msg: "no key was provided"} errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} diff --git a/securecookie.go b/securecookie.go index 5f38c98..a66b044 100644 --- a/securecookie.go +++ b/securecookie.go @@ -9,17 +9,18 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/rand" - "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/base64" - "encoding/gob" - "encoding/json" "fmt" "hash" - "io" "strconv" "time" + + "golang.org/x/crypto/nacl/secretbox" + + scrypt "github.com/elithrar/simple-scrypt" + "github.com/pkg/errors" ) const ( @@ -82,14 +83,13 @@ func New(key [32]byte, opts *Options) (*SecureCookie, error) { if opts.RotatedKeys != nil { for idx, v := range opts.RotatedKeys { - // ... + // TODO(matt): If there are keys, set tryKeys = true? + // Rotated keys are only used for decryption attempts. } } s := &SecureCookie{ - hashKey: hashKey, - blockKey: blockKey, - hashFunc: sha256.New, + key: key, maxAge: 86400 * 30, maxLength: 4096, sz: GobEncoder{}, @@ -102,11 +102,13 @@ func New(key [32]byte, opts *Options) (*SecureCookie, error) { // cookie values. type SecureCookie struct { key []byte - block cipher.AEAD + stretched bool + encrypter cipher.AEAD maxLength int maxAge int64 minAge int64 sz Serializer + opts *Options // For testing purposes, the function that returns the current timestamp. // If not set, it will use time.Now().UTC().Unix(). timeFunc func() int64 @@ -179,7 +181,7 @@ func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCo return s } -// Encoding sets the encoding/serialization method for cookies. +// SetSerializer sets the encoding/serialization method for cookies. // // Default is encoding/gob. To encode special structures using encoding/gob, // they must be registered first using gob.Register(). @@ -202,13 +204,46 @@ func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { // the current serialization/encryption settings on s and then base64-encoded, // is shorter than the maximum permissible length. func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { - if s.err != nil { - return "", s.err + // 1. Check that the key exists + // 2. Check whether we have KDF'ed the key yet (once only) + // 3. Serialize our payload + // 4. Check whether s.encrypt == true + // 5. Generate our payload: name.expiry.data + // 6. sign or encrypt + // 7. encode to base64 URL safe + // 8. Check that the maximum length does not exceed s.maxLenght (4096 by default) + // 9. Return string(encoded)able + + var encoded string + + if s.key == nil { + return encoded, errHashKeyNotSet + } + + // Run the provided key through a KDF (once only). + if !s.stretched { + var err error + if s.key, err = stretchKey(s.key); err != nil { + return "", errInvalidKey + } } - if s.hashKey == nil { - s.err = errHashKeyNotSet - return "", s.err + + var payload []byte + // TODO(matt): create a helper here: generatePayload? + if s.opts.EncryptCookies { + // Encrypt and early return + payload = []byte(fmt.Sprintf("%s|%d", name, s.timestamp())) + if payload, err = s.encrypt(payload); err != nil { + return "", errEncryptionFailed + } + } else { + // HMAC -> return } + + // base64.URLEncoding.EncodeToString + // Check length - len(encoded) > s.maxLength + // return encoded, nil + var err error var b []byte // 1. Serialize. @@ -334,16 +369,21 @@ func verifyMac(h hash.Hash, value []byte, mac []byte) error { // Authentication ------------------------------------------------------------- -// sign ... +// sign returns a signature for the provided data. // // Internally, sign uses HMAC-SHA-512/256, which is HMAC-SHA-512 truncated to a // 256-bit output to prevent length-extension attacks. func (s *SecureCookie) sign(data []byte) ([]byte, error) { + mac := hmac.New(sha512.New512_256, s.key) + mac.Write(data) - return nil, errSigningFailed + return mac.Sum(nil), nil } -// verify ... +// verify validates that the provided data matches the given signature. +// +// verify uses HMAC-SHA-512/256 for signatures and performs a constant-time +// comparison of signatures using Go's hmac.Equal function. func (s *SecureCookie) verify(data []byte, actualMAC []byte) bool { mac := hmac.New(sha512.New512_256, s.key) mac.Write(data) @@ -354,127 +394,67 @@ func (s *SecureCookie) verify(data []byte, actualMAC []byte) bool { // Encryption ----------------------------------------------------------------- -// encrypt encrypts the provided data, and returns a concatenation of nonce+ciphertext. +// encrypt encrypts the provided data using nacl/secretbox, and returns a +// concatenation of nonce+ciphertext. // -// Interally, encrypt uses ChaCha20+Poly1305 (an AEAD; combining a stream -// cipher & MAC construct) and generates a random, 96-bit nonce using Go's +// Interally, encrypt uses XSalsa20+Poly1305 (an AEAD; combining a stream +// cipher & MAC construct) and generates a random, 192-bit nonce using Go's // crypto/rand library, which leverages /dev/urandom or the equivalent on all -// platforms. +// platforms. A random nonce is used to prevent nonce re-use issues, and does +// not require the package or package user to increment nonces. func (s *SecureCookie) encrypt(data []byte) ([]byte, error) { - - return nil, errEncryptionFailed -} - -// decrypt decrypts the provided nonce+ciphertext. -func (s *SecureCookie) decrypt(nonceCiphertext []byte) ([]byte, error) { - - return nil, errDecryptionFailed -} - -// encrypt encrypts a value using the given block in counter mode. -// -// A random initialization vector (http://goo.gl/zF67k) with the length of the -// block size is prepended to the resulting ciphertext. -func encrypt(block cipher.Block, value []byte) ([]byte, error) { - iv := GenerateRandomKey(block.BlockSize()) - if iv == nil { - return nil, errGeneratingIV - } - // Encrypt it. - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(value, value) - // Return iv + ciphertext. - return append(iv, value...), nil -} - -// decrypt decrypts a value using the given block in counter mode. -// -// The value to be decrypted must be prepended by a initialization vector -// (http://goo.gl/zF67k) with the length of the block size. -func decrypt(block cipher.Block, value []byte) ([]byte, error) { - size := block.BlockSize() - if len(value) > size { - // Extract iv. - iv := value[:size] - // Extract ciphertext. - value = value[size:] - // Decrypt it. - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(value, value) - return value, nil - } - return nil, errDecryptionFailed -} - -// Serialization -------------------------------------------------------------- - -// Serialize encodes a value using gob. -func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { - buf := new(bytes.Buffer) - enc := gob.NewEncoder(buf) - if err := enc.Encode(src); err != nil { - return nil, cookieError{cause: err, typ: usageError} + // 1. Check our key is not nil + if s.key == nil { + return nil, errInvalidKey } - return buf.Bytes(), nil -} -// Deserialize decodes a value using gob. -func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { - dec := gob.NewDecoder(bytes.NewBuffer(src)) - if err := dec.Decode(dst); err != nil { - return cookieError{cause: err, typ: decodeError} + // 2. Check that our data is not nil + if data == nil { + return nil, errInvalidData } - return nil -} -// Serialize encodes a value using encoding/json. -func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { - buf := new(bytes.Buffer) - enc := json.NewEncoder(buf) - if err := enc.Encode(src); err != nil { - return nil, cookieError{cause: err, typ: usageError} + // 3. Generate a fresh 96 bit nonce + nonce, err := GenerateRandomBytes(12) + if err != nil { + return nil, errors.Wrap(err, "encryption failed") } - return buf.Bytes(), nil -} -// Deserialize decodes a value using encoding/json. -func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { - dec := json.NewDecoder(bytes.NewReader(src)) - if err := dec.Decode(dst); err != nil { - return cookieError{cause: err, typ: decodeError} - } - return nil + // 4. Encrypt our data, appending the ciphertext to the nonce. + return secretbox.Seal(nonce[:], data, nonce, s.key), nil } -// Serialize passes a []byte through as-is. -func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { - if b, ok := src.([]byte); ok { - return b, nil +// decrypt decrypts the provided nonce+ciphertext using nacl/secretbox. +// +// It expects that the the 196-bit nonce is prepended to the ciphertext. +func (s *SecureCookie) decrypt(encrypted []byte) ([]byte, error) { + if s.key == nil { + return nil, errInvalidKey } - return nil, errValueNotByte -} + if encrypted == nil || len(encrypted) < 24 { + return nil, errDecryptionFailed + } -// Deserialize passes a []byte through as-is. -func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { - if _, ok := dst.([]byte); ok { - dst = src - return nil + // 3. Parse our nonce + nonce := encrypted[24:] + ptext, ok := secretbox.Open([]byte{}, encrypted[24:], nonce, s.key) + if !ok { + return nil, errDecryptionFailed } - return errValueNotByte + return ptext, nil } // Encoding ------------------------------------------------------------------- -// encode encodes a value using base64. +// encode encodes a value using URL-safe base64. func encode(value []byte) []byte { encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) base64.URLEncoding.Encode(encoded, value) return encoded } -// decode decodes a cookie using base64. +// decode decodes a cookie using URL-safe base64. func decode(value []byte) ([]byte, error) { decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) b, err := base64.URLEncoding.Decode(decoded, value) @@ -486,19 +466,25 @@ func decode(value []byte) ([]byte, error) { // Helpers -------------------------------------------------------------------- -// GenerateRandomKey creates a random key with the given length in bytes. -// On failure, returns nil. -// -// Callers should explicitly check for the possibility of a nil return, treat -// it as a failure of the system random number generator, and not continue. -func GenerateRandomKey(length int) []byte { - k := make([]byte, length) - if _, err := io.ReadFull(rand.Reader, k); err != nil { - return nil - } - return k +// 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) + // Note that err == nil only if we read len(b) bytes. + return b, err } +// stretchKey passes the provided authentication/encryption key through a KDF +// (scrypt) to improve the entropy of the key used +func stretchKey(key []byte) ([]byte, error) { + return scrypt.GenerateFromPassword(key, scrypt.DefaultParams) +} + +// TODO(matt): RotatedEncrypt / RotatedDecrypt? + // CodecsFromPairs returns a slice of SecureCookie instances. // // It is a convenience function to create a list of codecs for key rotation. Note diff --git a/serialize.go b/serialize.go new file mode 100644 index 0000000..2d1e8e7 --- /dev/null +++ b/serialize.go @@ -0,0 +1,64 @@ +package securecookie + +import ( + "bytes" + "encoding/gob" + "encoding/json" +) + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if _, ok := dst.([]byte); ok { + dst = src + return nil + } + + return errValueNotByte +} |