зеркало из https://github.com/microsoft/iron-go.git
260 строки
6.3 KiB
Go
260 строки
6.3 KiB
Go
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 {
|
|
// KeyBits defines how large the signing key should be.
|
|
KeyBits uint
|
|
|
|
// Iteracts is the number of iterations to derive a key from the
|
|
// secret. Set to ` by default.
|
|
Iterations uint
|
|
|
|
// The size of the salt (random buffer used to ensure that two identical
|
|
// objects will generate a different encrypted result. Ignored if salt
|
|
// set explicitly.
|
|
SaltBits uint
|
|
|
|
// Hash returns a new hasher used to digest the cookie.
|
|
Hash func() hash.Hash
|
|
}
|
|
|
|
// An Encryption struct is contained in the Options object and used to
|
|
// configure how cookies are encrypted.
|
|
type Encryption struct {
|
|
// KeyBits defines how large the signing key should be.
|
|
KeyBits uint
|
|
|
|
// Iteracts is the number of iterations to derive a key from the
|
|
// secret. Set to ` by default.
|
|
Iterations uint
|
|
|
|
// The size of the salt (random buffer used to ensure that two identical
|
|
// objects will generate a different encrypted result. Ignored if salt
|
|
// set explicitly.
|
|
SaltBits uint
|
|
|
|
// Cipher is the cipher used to encrypt and decrypt the cookie.
|
|
Cipher CipherFactory
|
|
|
|
// IVBits is the number of IV bits to generate, ignored if the the IV
|
|
// property is set explicitly.
|
|
IVBits uint
|
|
}
|
|
|
|
// Options is passed into New() to configure the cookie options.
|
|
type Options struct {
|
|
// Secret key to use for encrypting/decrypting data.
|
|
Secret []byte
|
|
// TTL is the sealed object lifetime, infinite if zero. Defaults to zero.
|
|
TTL time.Duration
|
|
// Permitted clock skew for incoming expirations. Defaults to 60 seconds.
|
|
TimestampSkew time.Duration
|
|
// Local clock offset, defaults to zero.
|
|
LocalTimeOffset time.Duration
|
|
|
|
Encryption *Encryption
|
|
Integrity *Integrity
|
|
}
|
|
|
|
// fillDefaults creates a new Options object with default values filled in.
|
|
func (o Options) fillDefaults() Options {
|
|
if len(o.Secret) < 32 {
|
|
panic("iron-go: secret key may not be less than 32 bits")
|
|
}
|
|
|
|
if o.TimestampSkew == 0 {
|
|
o.TimestampSkew = time.Second * 60
|
|
}
|
|
|
|
if o.Encryption == nil {
|
|
o.Encryption = &Encryption{
|
|
IVBits: 16,
|
|
KeyBits: 256,
|
|
Iterations: 1,
|
|
SaltBits: 32,
|
|
Cipher: AES256,
|
|
}
|
|
}
|
|
|
|
if o.Integrity == nil {
|
|
o.Integrity = &Integrity{
|
|
Hash: sha256.New,
|
|
KeyBits: 256,
|
|
Iterations: 1,
|
|
SaltBits: 32,
|
|
}
|
|
}
|
|
|
|
return o
|
|
}
|
|
|
|
// New creates a new Vault which can seal and unseal Iron cookies.
|
|
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 }
|
|
|
|
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)
|
|
}
|
|
|
|
type hmacResult struct {
|
|
Digest []byte
|
|
Salt []byte
|
|
}
|
|
|
|
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 nil, err
|
|
}
|
|
|
|
return h.Sum(nil), nil
|
|
}
|
|
|
|
func (v *Vault) decrypt(msg *message) ([]byte, error) {
|
|
key := v.generateKey(v.opts.Encryption.KeyBits, v.opts.Encryption.Iterations, msg.Salt)
|
|
_, decrypt, err := v.opts.Encryption.Cipher(key, msg.IV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := make([]byte, len(msg.EncryptedBody))
|
|
decrypt.CryptBlocks(data, msg.EncryptedBody)
|
|
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.
|
|
// It takes some options, or nil to use defaults. It returns an
|
|
// UnsealError if the message is invalid.
|
|
func (v *Vault) Unseal(str string) ([]byte, error) {
|
|
msg := &message{}
|
|
if err := msg.Unpack(str); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1. Check expiration
|
|
|
|
if !msg.Expiration.IsZero() {
|
|
delta := msg.Expiration.Sub(time.Now().Add(v.opts.LocalTimeOffset))
|
|
if delta < -v.opts.TimestampSkew {
|
|
return nil, UnsealError{"Expired or invalid seal"}
|
|
}
|
|
}
|
|
|
|
// 2. Run the MAC digest against the message excluding our additional
|
|
// salt and hmac
|
|
|
|
digest, err := v.hmacWithPassword(msg.HMACSalt, msg.Base())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 3. Check the HMAC
|
|
|
|
if subtle.ConstantTimeCompare(digest, msg.HMAC) == 0 {
|
|
return nil, UnsealError{"Bad hmac value"}
|
|
}
|
|
|
|
// 4. Decrypt!
|
|
|
|
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())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 3. Generate the packed result
|
|
|
|
msg.HMACSalt = hmacSalt
|
|
msg.HMAC = digest
|
|
return msg.Pack(), nil
|
|
}
|