aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--errors.go1
-rw-r--r--securecookie.go234
-rw-r--r--serialize.go64
3 files changed, 175 insertions, 124 deletions
diff --git a/errors.go b/errors.go
index 479161a..2c77465 100644
--- a/errors.go
+++ b/errors.go
@@ -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
+}