зеркало из https://github.com/microsoft/iron-go.git
Add sealing support
This commit is contained in:
Родитель
ef075f8bdd
Коммит
367eb327bb
109
iron.go
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
|
||||
}
|
||||
|
|
63
iron_test.go
63
iron_test.go
|
@ -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)
|
||||
}
|
||||
|
|
13
readme.md
13
readme.md
|
@ -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!
|
||||
```
|
||||
|
|
47
support.go
47
support.go
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче