Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sign, encryption key rotation #243

Merged
merged 1 commit into from
Jan 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions security/anticsrf/anti_csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ func New(cfg *config.Config) (*AntiCSRF, error) {
Domain: c.cfg.StringDefault(keyPrefix+".domain", ""),
Path: c.cfg.StringDefault(keyPrefix+".path", "/"),
HTTPOnly: true,
// Based on aah server SSL configuration `http.Cookie.Secure` value is set
Secure: c.cfg.BoolDefault("server.ssl.enable", false),
SameSite: c.cfg.StringDefault(keyPrefix+".same_site", "Lax"),
SameSite: strings.ToLower(c.cfg.StringDefault(keyPrefix+".samesite", "")),
}

// Anti-CSRF cookie TTL, default is 24 hours
Expand All @@ -81,7 +82,9 @@ func New(cfg *config.Config) (*AntiCSRF, error) {

if c.cookieMgr, err = cookie.NewManager(opts,
c.cfg.StringDefault(keyPrefix+".sign_key", ""),
c.cfg.StringDefault(keyPrefix+".enc_key", "")); err != nil {
c.cfg.StringDefault(keyPrefix+".enc_key", ""),
c.cfg.StringDefault(keyPrefix+".old_sign_key", ""),
c.cfg.StringDefault(keyPrefix+".old_enc_key", "")); err != nil {
return nil, err
}

Expand Down
116 changes: 75 additions & 41 deletions security/cookie/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"

"aahframe.work/essentials"
Expand All @@ -29,26 +28,42 @@ var (
ErrSignVerificationIsFailed = errors.New("security/cookie: sign verification is failed")
)

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package methods
//___________________________________
//______________________________________________________________________________

// NewManager method returns the new cookie manager.
func NewManager(opts *Options, signKey, encKey string) (*Manager, error) {
m := &Manager{Options: opts, maxCookieSize: 4096, sha: "sha-256"}

// Sign key
m.isSignKey = !ess.IsStrEmpty(signKey)
if m.isSignKey {
m.signKey = []byte(signKey)
//
// Example:
//
// cookieMgr := cookie.NewManager(options)
// cookieMgr := cookie.NewManager(options, signKey, encKey)
// cookieMgr := cookie.NewManager(options, signKey, encKey, oldSignKey, oldEncKey)
func NewManager(opts *Options, keys ...string) (*Manager, error) {
m := &Manager{
Options: opts,
maxCookieSize: 4096, // 4kb
sha: "sha-256",
key: new(key),
oldKey: new(key),
}
if len(keys) >= 2 && !ess.IsStrEmpty(keys[0]) && !ess.IsStrEmpty(keys[1]) {
m.key.sign = []byte(keys[0])
m.key.enc = []byte(keys[1])
}
if len(keys) == 4 && !ess.IsStrEmpty(keys[2]) && !ess.IsStrEmpty(keys[3]) {
m.oldKey.sign = []byte(keys[2])
m.oldKey.enc = []byte(keys[3])
}

// Enc key
var err error
m.isEncKey = !ess.IsStrEmpty(encKey)
if m.isEncKey {
m.encKey = []byte(encKey)
if m.cipherBlock, err = aes.NewCipher(m.encKey); err != nil {
if len(m.key.enc) > 0 {
if m.key.cipherBlock, err = aes.NewCipher(m.key.enc); err != nil {
return nil, err
}
}
if len(m.oldKey.enc) > 0 {
if m.oldKey.cipherBlock, err = aes.NewCipher(m.oldKey.enc); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -78,6 +93,17 @@ func NewWithOptions(value string, opts *Options) *http.Cookie {
cookie.Expires = time.Unix(1, 0)
}

// SameSite attribute support in the cookie
// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
switch opts.SameSite {
case "lax":
cookie.SameSite = http.SameSiteLaxMode
case "strict":
cookie.SameSite = http.SameSiteStrictMode
default:
cookie.SameSite = http.SameSiteDefaultMode
}

return cookie
}

Expand All @@ -89,12 +115,9 @@ func NewWithOptions(value string, opts *Options) *http.Cookie {
type Manager struct {
Options *Options

isSignKey bool
signKey []byte
key *key
oldKey *key
sha string
isEncKey bool
encKey []byte
cipherBlock cipher.Block
maxCookieSize int
}

Expand All @@ -109,6 +132,12 @@ type Options struct {
SameSite string
}

type key struct {
sign []byte
enc []byte
cipherBlock cipher.Block
}

// New method creates new cookie instance for given value with cookie manager options.
func (m *Manager) New(value string) *http.Cookie {
return NewWithOptions(value, m.Options)
Expand All @@ -117,15 +146,7 @@ func (m *Manager) New(value string) *http.Cookie {
// Write method writes the given cookie value into response.
func (m *Manager) Write(w http.ResponseWriter, value string) {
c := m.New(value)
if v := c.String(); !ess.IsStrEmpty(v) {
// Adding `SameSite` setting
// https://tools.ietf.org/html/draft-west-first-party-cookies-07
//
// Currently Go doesn't have this attribute in `http.Cookie`, for future proof
// check and then add `SameSite` setting.
if !strings.Contains(v, "SameSite") && !ess.IsStrEmpty(m.Options.SameSite) {
v += "; SameSite=" + m.Options.SameSite
}
if v := c.String(); len(v) > 0 {
w.Header().Add("Set-Cookie", v)
}
}
Expand All @@ -139,8 +160,8 @@ func (m *Manager) Write(w http.ResponseWriter, value string) {
// 4) Checks max cookie size i.e 4Kb
func (m *Manager) Encode(b []byte) (string, error) {
// Encrypt it
if m.isEncKey {
b = acrypto.AESEncrypt(m.cipherBlock, b)
if len(m.key.enc) > 0 {
b = acrypto.AESEncrypt(m.key.cipherBlock, b)
}

// Encode it
Expand All @@ -150,8 +171,8 @@ func (m *Manager) Encode(b []byte) (string, error) {
b = []byte(fmt.Sprintf("%s|%d|%s|", m.Options.Name, currentTimestamp(), b))

// Sign it if enabled
if m.isSignKey {
signed := acrypto.Sign(m.signKey, b[:len(b)-1], m.sha)
if len(m.key.sign) > 0 {
signed := acrypto.Sign(m.key.sign, b[:len(b)-1], m.sha)

// Append signed value
b = append(b, signed...)
Expand Down Expand Up @@ -201,11 +222,22 @@ func (m *Manager) Decode(value string) ([]byte, error) {
b = append([]byte(m.Options.Name+"|"), b[:len(b)-len(parts[2])-1]...)

// Verify signed data, if enabled
if m.isSignKey {
if !acrypto.Verify(m.signKey, b, parts[2], m.sha) {
return nil, ErrSignVerificationIsFailed
var oldKey bool
if len(m.key.sign) > 0 {
if !acrypto.Verify(m.key.sign, b, parts[2], m.sha) {
if len(m.oldKey.sign) > 0 {
oldKey = true
if !acrypto.Verify(m.oldKey.sign, b, parts[2], m.sha) {
err = ErrSignVerificationIsFailed
}
} else {
err = ErrSignVerificationIsFailed
}
}
}
if err != nil {
return nil, err
}

// Verify timestamp
var t1 int64
Expand All @@ -220,18 +252,20 @@ func (m *Manager) Decode(value string) ([]byte, error) {
return nil, ErrCookieTimestampIsExpired
}

// Decrypt it
// Decode
b, err = ess.DecodeBase64(parts[1])
if err != nil {
return nil, err
}
if m.isEncKey {
if b, err = acrypto.AESDecrypt(m.cipherBlock, b); err != nil {
return nil, err
if len(m.key.enc) > 0 { // Decrypt
if oldKey {
b, err = acrypto.AESDecrypt(m.oldKey.cipherBlock, b)
} else {
b, err = acrypto.AESDecrypt(m.key.cipherBlock, b)
}
}

return b, nil
return b, err
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Expand Down
78 changes: 75 additions & 3 deletions security/cookie/cookie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,30 @@ func TestCookieNew(t *testing.T) {
assert.Equal(t, -1, cookie.MaxAge)
}

func TestCookieManager(t *testing.T) {
func TestCookieWithNoKeys(t *testing.T) {
cm, err := NewManager(&Options{
Name: "aah",
MaxAge: 1800,
})
assert.Nil(t, err)
assert.NotNil(t, cm)

value := "im the cookie with no keys"

encodeValue, err := cm.Encode([]byte(value))
assert.Nil(t, err)
assert.True(t, len(encodeValue) > 0)

c1 := cm.New(encodeValue)
assert.NotNil(t, c1)
assert.True(t, len(c1.Value) > 0)

r1, err := cm.Decode(encodeValue)
assert.Nil(t, err)
assert.Equal(t, value, string(r1))
}

func TestCookieWithKeys(t *testing.T) {
opts := &Options{
Name: "aah",
MaxAge: 1800,
Expand All @@ -55,9 +78,13 @@ func TestCookieManager(t *testing.T) {
hdr := w.Header().Get("Set-Cookie")
assert.True(t, strings.Contains(hdr, value))

cookie := cm.New(value)
cookie := cm.New(result)
assert.NotNil(t, cookie)

r2, err := cm.Decode(result)
assert.Nil(t, err)
assert.Equal(t, value, string(r2))

_, err = cm.Decode("MTQ5MTM2OTI4NXxpV1l2SHZrc0tZaXprdlA5Ql9ZS3RWOC1yOFVoWElack1VTGJIM01aV2dGdmJvamJOR2Rmc05KQW1SeHNTS2FoNEJLY2NFN2MyenVCbGllaU1NRFV88hn8MIb0L5HFU6GAkvwYjQ1rvmaL3lG3am2ZageHxQ0=")
assert.Equal(t, ErrSignVerificationIsFailed, err)

Expand All @@ -68,6 +95,51 @@ func TestCookieManager(t *testing.T) {
_, err = cm.Decode(string(bvalue))
assert.Equal(t, ErrCookieValueIsInvalid, err)

_, err = cm.Decode(value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value + value)
var dvalue string
for i := 0; i < 70; i++ {
dvalue = dvalue + value
}
_, err = cm.Decode(dvalue)
assert.Equal(t, ErrCookieValueIsTooLarge, err)
}

func TestCookieWithKeysRotation(t *testing.T) {
opts := &Options{
Name: "aah",
MaxAge: 1 << 31,
SameSite: "lax",
}

cmo, _ := NewManager(opts, "eFWLXEewECptbDVXExokRTLONWxrTold", "KYqklJsgeclPpZutTeQKNOTWlpksRold")
assert.NotNil(t, cmo)

value := "im the cookie with new and old keys"
c1 := cmo.New(value)
assert.NotNil(t, c1)
assert.Equal(t, value, c1.Value)

encodeValue, err := cmo.Encode([]byte(value))
assert.Nil(t, err)
assert.True(t, len(encodeValue) > 0)

r1, err := cmo.Decode(encodeValue)
assert.Nil(t, err)
assert.Equal(t, value, string(r1))

opts.SameSite = "strict"
cmr, err := NewManager(opts, "eFWLXEewECptbDVXExokRTLONWxrTjfV", "KYqklJsgeclPpZutTeQKNOTWlpksRBwA",
"eFWLXEewECptbDVXExokRTLONWxrTold", "KYqklJsgeclPpZutTeQKNOTWlpksRold")
assert.Nil(t, err)

c2 := cmr.New(value)
assert.NotNil(t, c1)
assert.Equal(t, value, c2.Value)

r2, err := cmr.Decode(encodeValue)
assert.Nil(t, err)
assert.Equal(t, value, string(r2))

_, err = cmr.Decode("MTQ5MTM2OTI4NXxpV1l2SHZrc0tZaXprdlA5Ql9ZS3RWOC1yOFVoWElack1VTGJIM01aV2dGdmJvamJOR2Rmc05KQW1SeHNTS2FoNEJLY2NFN2MyenVCbGllaU1NRFV88hn8MIb0L5HFU6GAkvwYjQ1rvmaL3lG3am2ZageHxQ0=")
assert.NotNil(t, err)
assert.Equal(t, ErrSignVerificationIsFailed, err)
}
8 changes: 6 additions & 2 deletions security/session/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ func NewManager(appCfg *config.Config) (*Manager, error) {
Path: m.cfg.StringDefault(keyPrefix+".path", "/"),
HTTPOnly: m.cfg.BoolDefault(keyPrefix+".http_only", true),
// Based on aah server SSL configuration `http.Cookie.Secure` value is set
Secure: m.cfg.BoolDefault("server.ssl.enable", false),
Secure: m.cfg.BoolDefault("server.ssl.enable", false),
SameSite: strings.ToLower(m.cfg.StringDefault(keyPrefix+".samesite", "")),
}

// TTL value
Expand All @@ -117,7 +118,10 @@ func NewManager(appCfg *config.Config) (*Manager, error) {

m.cookieMgr, err = cookie.NewManager(opts,
m.cfg.StringDefault(keyPrefix+".sign_key", ""),
m.cfg.StringDefault(keyPrefix+".enc_key", ""))
m.cfg.StringDefault(keyPrefix+".enc_key", ""),
m.cfg.StringDefault(keyPrefix+".old_sign_key", ""),
m.cfg.StringDefault(keyPrefix+".old_enc_key", ""),
)
if err != nil {
return nil, err
}
Expand Down