github.com/naoina/kocha@v0.7.1-0.20171129072645-78c7a531f799/session.go (about) 1 package kocha 2 3 import ( 4 "bytes" 5 "crypto/aes" 6 "crypto/cipher" 7 "crypto/hmac" 8 "crypto/rand" 9 "crypto/sha512" 10 "encoding/base64" 11 "errors" 12 "fmt" 13 "io" 14 15 "github.com/ugorji/go/codec" 16 ) 17 18 // SessionStore is the interface that session store. 19 type SessionStore interface { 20 Save(sess Session) (key string, err error) 21 Load(key string) (sess Session, err error) 22 } 23 24 // Session represents a session data store. 25 type Session map[string]string 26 27 // Get gets a value associated with the given key. 28 // If there is the no value associated with the given key, Get returns "". 29 func (sess Session) Get(key string) string { 30 return sess[key] 31 } 32 33 // Set sets the value associated with the key. 34 // If replaces the existing value associated with the key. 35 func (sess Session) Set(key, value string) { 36 sess[key] = value 37 } 38 39 // Del deletes the value associated with the key. 40 func (sess Session) Del(key string) { 41 delete(sess, key) 42 } 43 44 // Clear clear the all session data. 45 func (sess Session) Clear() { 46 for k, _ := range sess { 47 delete(sess, k) 48 } 49 } 50 51 type ErrSession struct { 52 msg string 53 } 54 55 func (e ErrSession) Error() string { 56 return e.msg 57 } 58 59 func NewErrSession(msg string) error { 60 return ErrSession{ 61 msg: msg, 62 } 63 } 64 65 // Implementation of cookie store. 66 // 67 // This session store will be a session save to client-side cookie. 68 // Session cookie for save is encoded, encrypted and signed. 69 type SessionCookieStore struct { 70 // key for the encryption. 71 SecretKey string 72 73 // Key for the cookie singing. 74 SigningKey string 75 } 76 77 var codecHandler = &codec.MsgpackHandle{} 78 79 // Save saves and returns the key of session cookie. 80 // Actually, key is session cookie data itself. 81 func (store *SessionCookieStore) Save(sess Session) (key string, err error) { 82 buf := bufPool.Get().(*bytes.Buffer) 83 defer func() { 84 buf.Reset() 85 bufPool.Put(buf) 86 }() 87 if err := codec.NewEncoder(buf, codecHandler).Encode(sess); err != nil { 88 return "", err 89 } 90 encrypted, err := store.encrypt(buf.Bytes()) 91 if err != nil { 92 return "", err 93 } 94 return store.encode(store.sign(encrypted)), nil 95 } 96 97 // Load returns the session data that extract from cookie value. 98 // The key is stored session cookie value. 99 func (store *SessionCookieStore) Load(key string) (sess Session, err error) { 100 decoded, err := store.decode(key) 101 if err != nil { 102 return nil, err 103 } 104 unsigned, err := store.verify(decoded) 105 if err != nil { 106 return nil, err 107 } 108 decrypted, err := store.decrypt(unsigned) 109 if err != nil { 110 return nil, err 111 } 112 if err := codec.NewDecoderBytes(decrypted, codecHandler).Decode(&sess); err != nil { 113 return nil, err 114 } 115 return sess, nil 116 } 117 118 // Validate validates SecretKey size. 119 func (store *SessionCookieStore) Validate() error { 120 b, err := base64.StdEncoding.DecodeString(store.SecretKey) 121 if err != nil { 122 return err 123 } 124 store.SecretKey = string(b) 125 b, err = base64.StdEncoding.DecodeString(store.SigningKey) 126 if err != nil { 127 return err 128 } 129 store.SigningKey = string(b) 130 switch len(store.SecretKey) { 131 case 16, 24, 32: 132 return nil 133 } 134 return fmt.Errorf("kocha: session: %T.SecretKey size must be 16, 24 or 32, but %v", *store, len(store.SecretKey)) 135 } 136 137 // encrypt returns encrypted data by AES-256-CBC. 138 func (store *SessionCookieStore) encrypt(buf []byte) ([]byte, error) { 139 block, err := aes.NewCipher([]byte(store.SecretKey)) 140 if err != nil { 141 return nil, err 142 } 143 aead, err := cipher.NewGCM(block) 144 if err != nil { 145 return nil, err 146 } 147 iv := make([]byte, aead.NonceSize(), len(buf)+aead.NonceSize()) 148 if _, err := io.ReadFull(rand.Reader, iv); err != nil { 149 return nil, err 150 } 151 encrypted := aead.Seal(nil, iv, buf, nil) 152 return append(iv, encrypted...), nil 153 } 154 155 // decrypt returns decrypted data from crypted data by AES-256-CBC. 156 func (store *SessionCookieStore) decrypt(buf []byte) ([]byte, error) { 157 block, err := aes.NewCipher([]byte(store.SecretKey)) 158 if err != nil { 159 return nil, err 160 } 161 aead, err := cipher.NewGCM(block) 162 if err != nil { 163 return nil, err 164 } 165 iv := buf[:aead.NonceSize()] 166 decrypted := buf[aead.NonceSize():] 167 if _, err := aead.Open(decrypted[:0], iv, decrypted, nil); err != nil { 168 return nil, err 169 } 170 return decrypted, nil 171 } 172 173 // encode returns encoded string by Base64 with URLEncoding. 174 // However, encoded string will stripped the padding character of Base64. 175 func (store *SessionCookieStore) encode(src []byte) string { 176 buf := make([]byte, base64.URLEncoding.EncodedLen(len(src))) 177 base64.URLEncoding.Encode(buf, src) 178 for { 179 if buf[len(buf)-1] != '=' { 180 break 181 } 182 buf = buf[:len(buf)-1] 183 } 184 return string(buf) 185 } 186 187 // decode returns decoded data from encoded data by Base64 with URLEncoding. 188 func (store *SessionCookieStore) decode(src string) ([]byte, error) { 189 size := len(src) 190 rem := (4 - size%4) % 4 191 buf := make([]byte, size+rem) 192 copy(buf, src) 193 for i := 0; i < rem; i++ { 194 buf[size+i] = '=' 195 } 196 n, err := base64.URLEncoding.Decode(buf, buf) 197 if err != nil { 198 return nil, err 199 } 200 return buf[:n], nil 201 } 202 203 // sign returns signed data. 204 func (store *SessionCookieStore) sign(src []byte) []byte { 205 sign := store.hash(src) 206 return append(sign, src...) 207 } 208 209 // verify verify signed data and returns unsigned data if valid. 210 func (store *SessionCookieStore) verify(src []byte) (unsigned []byte, err error) { 211 if len(src) <= sha512.Size256 { 212 return nil, errors.New("kocha: session cookie value too short") 213 } 214 sign := src[:sha512.Size256] 215 unsigned = src[sha512.Size256:] 216 if !hmac.Equal(store.hash(unsigned), sign) { 217 return nil, errors.New("kocha: session cookie verification failed") 218 } 219 return unsigned, nil 220 } 221 222 // hash returns hashed data by HMAC-SHA512/256. 223 func (store *SessionCookieStore) hash(src []byte) []byte { 224 hash := hmac.New(sha512.New512_256, []byte(store.SigningKey)) 225 hash.Write(src) 226 return hash.Sum(nil) 227 }