This commit is contained in:
Connor Peet 2016-06-02 12:02:44 -07:00
Родитель ef075f8bdd
Коммит 367eb327bb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CF8FD2EA0DBC61BD
4 изменённых файлов: 199 добавлений и 33 удалений

109
iron.go
Просмотреть файл

@ -1,16 +1,23 @@
package iron
import (
"bytes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"hash"
"time"
"golang.org/x/crypto/pbkdf2"
)
// Padding symbol used by Iron. This will be added when encrypting and trimmed
// out when decrypting.
const padder = '\t'
// An Integrity struct is contained in the Options struct and describes
// configuration for cookie integrity verification.
type Integrity struct {
@ -80,10 +87,10 @@ func (o Options) fillDefaults() Options {
if o.Encryption == nil {
o.Encryption = &Encryption{
IVBits: 128,
IVBits: 16,
KeyBits: 256,
Iterations: 1,
SaltBits: 256,
SaltBits: 32,
Cipher: AES256,
}
}
@ -93,7 +100,7 @@ func (o Options) fillDefaults() Options {
Hash: sha256.New,
KeyBits: 256,
Iterations: 1,
SaltBits: 256,
SaltBits: 32,
}
}
@ -106,11 +113,6 @@ func New(options Options) *Vault { return &Vault{options.fillDefaults()} }
// Vault is a structure capable is sealing and unsealing Iron cookies.
type Vault struct{ opts Options }
var (
macFormatVersion = "2"
expectedMacPrefix = "Fe26." + macFormatVersion
)
func (v *Vault) generateKey(keybits uint, iterations uint, salt []byte) []byte {
return pbkdf2.Key(v.opts.Secret, salt, int(iterations), int(keybits/8), sha1.New)
}
@ -120,15 +122,14 @@ type hmacResult struct {
Salt []byte
}
func (v *Vault) hmacWithPassword(salt []byte, data string) (out hmacResult, err error) {
func (v *Vault) hmacWithPassword(salt []byte, data string) (digest []byte, err error) {
key := v.generateKey(v.opts.Integrity.KeyBits, v.opts.Integrity.Iterations, salt)
h := hmac.New(v.opts.Integrity.Hash, key)
if _, err := h.Write([]byte(data)); err != nil {
return out, err
return nil, err
}
out.Digest = h.Sum(nil)
return out, nil
return h.Sum(nil), nil
}
func (v *Vault) decrypt(msg *message) ([]byte, error) {
@ -140,7 +141,53 @@ func (v *Vault) decrypt(msg *message) ([]byte, error) {
data := make([]byte, len(msg.EncryptedBody))
decrypt.CryptBlocks(data, msg.EncryptedBody)
return data, nil
return bytes.TrimRight(data, string(padder)), nil
}
func (v *Vault) generateSalt(size uint) ([]byte, error) {
rawSalt, err := randBits(v.opts.Encryption.SaltBits)
if err != nil {
return nil, err
}
salt := make([]byte, base64.RawURLEncoding.EncodedLen(len(rawSalt)))
base64.RawURLEncoding.Encode(salt, rawSalt)
return salt, nil
}
func (v *Vault) encryptBlocks(block cipher.BlockMode, b []byte) []byte {
size := block.BlockSize()
b = append(b, bytes.Repeat([]byte{padder}, size-len(b)%size)...)
out := make([]byte, len(b))
for i := 0; i < len(b); i += size {
block.CryptBlocks(out[i:i+size], b[i:i+size])
}
return out
}
func (v *Vault) encrypt(b []byte) (*message, error) {
salt, err := v.generateSalt(v.opts.Encryption.SaltBits)
if err != nil {
return nil, err
}
key := v.generateKey(v.opts.Encryption.KeyBits, v.opts.Encryption.Iterations, salt)
iv, err := randBits(v.opts.Encryption.IVBits)
if err != nil {
return nil, err
}
encrypt, _, err := v.opts.Encryption.Cipher(key, iv)
if err != nil {
return nil, err
}
return &message{
EncryptedBody: v.encryptBlocks(encrypt, b),
IV: iv,
Salt: salt,
}, nil
}
// Unseal attempts to extract the encrypted information from the message.
@ -155,8 +202,8 @@ func (v *Vault) Unseal(str string) ([]byte, error) {
// 1. Check expiration
if !msg.Expiration.IsZero() {
delta := time.Now().Add(v.opts.LocalTimeOffset).Sub(msg.Expiration)
if delta > v.opts.TimestampSkew || delta < v.opts.TimestampSkew {
delta := msg.Expiration.Sub(time.Now().Add(v.opts.LocalTimeOffset))
if delta < -v.opts.TimestampSkew {
return nil, UnsealError{"Expired or invalid seal"}
}
}
@ -164,14 +211,14 @@ func (v *Vault) Unseal(str string) ([]byte, error) {
// 2. Run the MAC digest against the message excluding our additional
// salt and hmac
mac, err := v.hmacWithPassword(msg.HMACSalt, msg.Base())
digest, err := v.hmacWithPassword(msg.HMACSalt, msg.Base())
if err != nil {
return nil, err
}
// 3. Check the HMAC
if subtle.ConstantTimeCompare(mac.Digest, msg.HMAC) == 0 {
if subtle.ConstantTimeCompare(digest, msg.HMAC) == 0 {
return nil, UnsealError{"Bad hmac value"}
}
@ -179,3 +226,31 @@ func (v *Vault) Unseal(str string) ([]byte, error) {
return v.decrypt(msg)
}
// Seal encrypts and signs the byte slice into an Iron cookie.
func (v *Vault) Seal(b []byte) (string, error) {
// 1. Encrypt the payload
msg, err := v.encrypt(b)
if err != nil {
return "", err
}
if v.opts.TTL > 0 {
msg.Expiration = time.Now().Add(v.opts.TTL)
}
// 2. Generate an HMAC signature
hmacSalt, err := v.generateSalt(v.opts.Integrity.SaltBits)
if err != nil {
return "", err
}
digest, err := v.hmacWithPassword(hmacSalt, msg.Base())
// 3. Generate the packed result
msg.HMACSalt = hmacSalt
msg.HMAC = digest
return msg.Pack(), nil
}

Просмотреть файл

@ -3,12 +3,14 @@ package iron
import (
"encoding/base64"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var (
password = []byte(`some_not_random_password_that_is_also_long_enough`)
source = []byte(`{"a":1,"b":2,"c":[3,4,5],"d":{"e":"f"}}`)
salt = []byte(`e4fe33b6dc4c7ef5ad7907f015deb7b03723b03a54764aceeb2ab1235cc8dce3`)
)
@ -42,12 +44,65 @@ var (
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
func TestPanicsOnShortPassword(t *testing.T) {
assert.Panics(t, func() {
New(Options{Secret: []byte(`hi`)})
})
}
func TestSealsAndParses(t *testing.T) {
v := New(Options{Secret: password})
cookie, err := v.Seal(source)
assert.Nil(t, err)
payload, err := v.Unseal(cookie)
assert.Nil(t, err)
assert.Equal(t, source, payload)
}
func TestSealsWithExpiration(t *testing.T) {
v := New(Options{Secret: password, TTL: time.Hour})
cookie, err := v.Seal(source)
assert.Nil(t, err)
payload, err := v.Unseal(cookie)
assert.Nil(t, err)
assert.Equal(t, source, payload)
v2 := New(Options{Secret: password, TTL: time.Hour, LocalTimeOffset: time.Hour * 2})
_, err = v2.Unseal(cookie)
assert.Equal(t, UnsealError{"Expired or invalid seal"}, err)
}
func TestSealsWithExpirationAndTimeShift(t *testing.T) {
v := New(Options{Secret: password, TTL: time.Hour})
cookie, err := v.Seal(source)
assert.Nil(t, err)
payload, err := v.Unseal(cookie)
assert.Nil(t, err)
assert.Equal(t, source, payload)
v2 := New(Options{
Secret: password, TTL: time.Hour,
LocalTimeOffset: time.Hour + 40*time.Second,
TimestampSkew: 60 * time.Second,
})
_, err = v2.Unseal(cookie)
assert.Nil(t, err)
v2.opts.LocalTimeOffset = time.Hour + 61*time.Second
_, err = v2.Unseal(cookie)
assert.Equal(t, UnsealError{"Expired or invalid seal"}, err)
}
func TestUnsealsTicket(t *testing.T) {
v := New(Options{Secret: password})
payload, err := v.Unseal("Fe26.2**0cdd607945dd1dffb7da0b0bf5f1a7daa6218cbae14cac51dcbd91fb077aeb5b*aOZLCKLhCt0D5IU1qLTtYw*g0ilNDlQ3TsdFUqJCqAm9iL7Wa60H7eYcHL_5oP136TOJREkS3BzheDC1dlxz5oJ**05b8943049af490e913bbc3a2485bee2aaf7b823f4c41d0ff0b7c168371a3772*R8yscVdTBRMdsoVbdDiFmUL8zb-c3PQLGJn4Y8C-AqI")
assert.Nil(t, err)
// all those tabs are in Iron's tests for some reason. I'll just leave them :P
assert.Equal(t, "{\"a\":1,\"b\":2,\"c\":[3,4,5],\"d\":{\"e\":\"f\"}}\t\t\t\t\t\t\t\t\t", string(payload))
assert.Equal(t, "{\"a\":1,\"b\":2,\"c\":[3,4,5],\"d\":{\"e\":\"f\"}}", string(payload))
}
func TestReturnsErrWithWrongUnseals(t *testing.T) {
@ -82,8 +137,8 @@ func TestReturnsErrOnExpired(t *testing.T) {
assert.Nil(t, err)
_, err = v.Unseal(base + "*" +
base64.RawURLEncoding.EncodeToString(mac.Salt) + "*" +
base64.RawURLEncoding.EncodeToString(mac.Digest))
base64.RawURLEncoding.EncodeToString(salt) + "*" +
base64.RawURLEncoding.EncodeToString(mac))
assert.Equal(t, UnsealError{"Expired or invalid seal"}, err)
}

Просмотреть файл

@ -1,12 +1,19 @@
# iron-go [![Build Status](https://travis-ci.org/WatchBeam/iron-go.svg?branch=master)](https://travis-ci.org/WatchBeam/iron-go) [![godoc reference](https://godoc.org/github.com/WatchBeam/iron-go?status.png)](https://godoc.org/github.com/WatchBeam/iron-go)
iron-go is an implementation of [Iron](https://github.com/hueniverse/iron) cookies for Go. It's fully inter-operable with the Node version. Currently it just supports unsealing using a single secret key, but it should be fairly trivial to implement sealing and rotation in the future. <sup>[Citation Needed]</sup>
iron-go is an implementation of [Iron](https://github.com/hueniverse/iron) cookies for Go. It's fully inter-operable with the Node version. Currently it supports sealing and unsealing using a single secret key, but it should be fairly trivial to implement rotation in the future. <sup>[Citation Needed]</sup>
```go
v := iron.New(Options{Secret: password})
payload, err := v.Unseal(yourCookie)
// unmarshal the payload JSON as you see fit!
// encrypt your cookie:
cookie, err := v.Seal(yourData)
// Later....
payload, err := v.Unseal(cookie)
// Use your data!
```

Просмотреть файл

@ -8,6 +8,12 @@ import (
"time"
)
var (
macFormatVersion = "2"
macPrefix = "Fe26." + macFormatVersion
delimiter = "*"
)
type message struct {
base string // this is the cookie message excluding the hmac and salt
@ -22,11 +28,11 @@ type message struct {
// Unpack attempts to populate the message by unmarshaling the provided string.
// It returns an UnsealError if the string isn't valid.
func (m *message) Unpack(s string) error {
parts := strings.Split(s, "*")
parts := strings.Split(s, delimiter)
if len(parts) != 8 {
return UnsealError{"Incorrect number of sealed components"}
}
if parts[0] != expectedMacPrefix {
if parts[0] != macPrefix {
return UnsealError{"Wrong mac prefix"}
}
if len(parts[5]) > 0 {
@ -34,13 +40,13 @@ func (m *message) Unpack(s string) error {
if err != nil {
return UnsealError{"Invalid expiration time"}
}
m.Expiration = time.Unix(0, exp)
m.Expiration = time.Unix(0, exp*int64(time.Millisecond))
}
errs := []error{
basee64decodeInto(&m.IV, parts[3]),
basee64decodeInto(&m.EncryptedBody, parts[4]),
basee64decodeInto(&m.HMAC, parts[7]),
base64decodeInto(&m.IV, parts[3]),
base64decodeInto(&m.EncryptedBody, parts[4]),
base64decodeInto(&m.HMAC, parts[7]),
}
for _, err := range errs {
@ -55,6 +61,15 @@ func (m *message) Unpack(s string) error {
return nil
}
// Pack serializes the message into a cookie string.
func (m *message) Pack() string {
return strings.Join([]string{
m.Base(),
string(m.HMACSalt),
base64.RawURLEncoding.EncodeToString(m.HMAC),
}, delimiter)
}
// Base returns the MAC base string, which is the cookie excluding the
// salt and hmac components.
func (m *message) Base() string {
@ -62,12 +77,26 @@ func (m *message) Base() string {
return m.base
}
panic("base string not set")
parts := []string{
macPrefix,
"", // todo: password rotation component
string(m.Salt),
base64.RawURLEncoding.EncodeToString(m.IV),
base64.RawURLEncoding.EncodeToString(m.EncryptedBody),
"",
}
if !m.Expiration.IsZero() {
parts[5] = strconv.FormatInt(m.Expiration.UnixNano()/int64(time.Millisecond), 10)
}
m.base = strings.Join(parts, delimiter)
return m.base
}
// basee64decodeInto attempts to base64 decode the source string into the
// base64decodeInto attempts to base64 decode the source string into the
// target address. It returns an error if the source is invalid.
func basee64decodeInto(target *[]byte, src string) error {
func base64decodeInto(target *[]byte, src string) error {
res, err := base64.RawURLEncoding.DecodeString(src)
*target = res
return err