1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-12-12 11:15:02 +02:00
oauth2-proxy/pkg/encryption/cipher.go
Nick Meves a09eecc6a2
Reduce SessionState size better with MessagePack + LZ4 (#632)
* Encode sessions with MsgPack + LZ4

Assumes ciphers are now mandatory per #414. Cookie & Redis sessions
can fallback to V5 style JSON in error cases. TODO: session_state.go
unit tests & new unit tests for Legacy fallback scenarios.

* Only compress encoded sessions with Cookie Store

* Cleanup msgpack + lz4 error handling

* Change NewBase64Cipher to take in an existing Cipher

* Add msgpack & lz4 session state tests

* Add required options for oauthproxy tests

More aggressively assert.NoError on all
validation.Validate(opts) calls to enforce legal
options in all our tests.
Add additional NoError checks wherever error return
values were ignored.

* Remove support for uncompressed session state fields

* Improve error verbosity & add session state tests

* Ensure all marshalled sessions are valid

Invalid CFB decryptions can result in garbage data
that 1/100 times might cause message pack unmarshal
to not fail and instead return an empty session.
This adds more rigor to make sure legacy sessions
cause appropriate errors.

* Add tests for legacy V5 session decoding

Refactor common legacy JSON test cases to a
legacy helpers area under session store tests.

* Make ValidateSession a struct method & add CHANGELOG entry

* Improve SessionState error & comments verbosity

* Move legacy session test helpers to sessions pkg

Placing these helpers under the sessions pkg removed
all the circular import uses in housing it under the
session store area.

* Improve SignatureAuthenticator test helper formatting

* Make redis.legacyV5DecodeSession internal

* Make LegacyV5TestCase test table public for linter
2020-07-13 20:56:05 +01:00

133 lines
3.4 KiB
Go

package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)
// Cipher provides methods to encrypt and decrypt
type Cipher interface {
Encrypt(value []byte) ([]byte, error)
Decrypt(ciphertext []byte) ([]byte, error)
}
type base64Cipher struct {
Cipher Cipher
}
// NewBase64Cipher returns a new AES Cipher for encrypting cookie values
// and wrapping them in Base64 -- Supports Legacy encryption scheme
func NewBase64Cipher(c Cipher) Cipher {
return &base64Cipher{Cipher: c}
}
// Encrypt encrypts a value with the embedded Cipher & Base64 encodes it
func (c *base64Cipher) Encrypt(value []byte) ([]byte, error) {
encrypted, err := c.Cipher.Encrypt(value)
if err != nil {
return nil, err
}
return []byte(base64.StdEncoding.EncodeToString(encrypted)), nil
}
// Decrypt Base64 decodes a value & decrypts it with the embedded Cipher
func (c *base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) {
encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext))
if err != nil {
return nil, fmt.Errorf("failed to base64 decode value %s", err)
}
return c.Cipher.Decrypt(encrypted)
}
type cfbCipher struct {
cipher.Block
}
// NewCFBCipher returns a new AES CFB Cipher
func NewCFBCipher(secret []byte) (Cipher, error) {
c, err := aes.NewCipher(secret)
if err != nil {
return nil, err
}
return &cfbCipher{Block: c}, err
}
// Encrypt with AES CFB
func (c *cfbCipher) Encrypt(value []byte) ([]byte, error) {
ciphertext := make([]byte, aes.BlockSize+len(value))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, fmt.Errorf("failed to create initialization vector %s", err)
}
stream := cipher.NewCFBEncrypter(c.Block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], value)
return ciphertext, nil
}
// Decrypt an AES CFB ciphertext
func (c *cfbCipher) Decrypt(ciphertext []byte) ([]byte, error) {
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("encrypted value should be at least %d bytes, but is only %d bytes", aes.BlockSize, len(ciphertext))
}
iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:]
plaintext := make([]byte, len(ciphertext))
stream := cipher.NewCFBDecrypter(c.Block, iv)
stream.XORKeyStream(plaintext, ciphertext)
return plaintext, nil
}
type gcmCipher struct {
cipher.Block
}
// NewGCMCipher returns a new AES GCM Cipher
func NewGCMCipher(secret []byte) (Cipher, error) {
c, err := aes.NewCipher(secret)
if err != nil {
return nil, err
}
return &gcmCipher{Block: c}, err
}
// Encrypt with AES GCM on raw bytes
func (c *gcmCipher) Encrypt(value []byte) ([]byte, error) {
gcm, err := cipher.NewGCM(c.Block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Using nonce as Seal's dst argument results in it being the first
// chunk of bytes in the ciphertext. Decrypt retrieves the nonce/IV from this.
ciphertext := gcm.Seal(nonce, nonce, value, nil)
return ciphertext, nil
}
// Decrypt an AES GCM ciphertext
func (c *gcmCipher) Decrypt(ciphertext []byte) ([]byte, error) {
gcm, err := cipher.NewGCM(c.Block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}