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  }