mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 11:04:25 +00:00
4e3fd305ab
# Which Problems Are Solved We noticed logging where 500: Internal Server errors were returned from the token endpoint, mostly for the `refresh_token` grant. The error was thrown by the database as it received non-UTF8 strings for token IDs Zitadel uses symmetric encryption for opaque tokens, including refresh tokens. Encrypted values are base64 encoded. It appeared to be possible to send garbage base64 to the token endpoint, which will pass decryption and string-splitting. In those cases the resulting ID is not a valid UTF-8 string. Invalid non-UTF8 strings are now rejected during token decryption. # How the Problems Are Solved - `AESCrypto.DecryptString()` checks if the decrypted bytes only contain valid UTF-8 characters before converting them into a string. - `AESCrypto.Decrypt()` is unmodified and still allows decryption on non-UTF8 byte strings. - `FromRefreshToken` now uses `DecryptString` instead of `Decrypt` # Additional Changes - Unit tests added for `FromRefreshToken` and `AESCrypto.DecryptString()`. - Fuzz tests added for `FromRefreshToken` and `AESCrypto.DecryptString()`. This was to pinpoint the problem - Testdata with values that resulted in invalid strings are committed. In the pipeline this results in the Fuzz tests to execute as regular unit-test cases. As we don't use the `-fuzz` flag in the pipeline no further fuzzing is performed. # Additional Context - Closes #7765 - https://go.dev/doc/tutorial/fuzz
147 lines
3.4 KiB
Go
147 lines
3.4 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"io"
|
|
"unicode/utf8"
|
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
var _ EncryptionAlgorithm = (*AESCrypto)(nil)
|
|
|
|
type AESCrypto struct {
|
|
keys map[string]string
|
|
encryptionKeyID string
|
|
keyIDs []string
|
|
}
|
|
|
|
func NewAESCrypto(config *KeyConfig, keyStorage KeyStorage) (*AESCrypto, error) {
|
|
keys, ids, err := LoadKeys(config, keyStorage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AESCrypto{
|
|
keys: keys,
|
|
encryptionKeyID: config.EncryptionKeyID,
|
|
keyIDs: ids,
|
|
}, nil
|
|
}
|
|
|
|
func (a *AESCrypto) Algorithm() string {
|
|
return "aes"
|
|
}
|
|
|
|
func (a *AESCrypto) Encrypt(value []byte) ([]byte, error) {
|
|
return EncryptAES(value, a.encryptionKey())
|
|
}
|
|
|
|
func (a *AESCrypto) Decrypt(value []byte, keyID string) ([]byte, error) {
|
|
key, err := a.decryptionKey(keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return DecryptAES(value, key)
|
|
}
|
|
|
|
// DecryptString decrypts the value using the key identified by keyID.
|
|
// When the decrypted value contains non-UTF8 characters an error is returned.
|
|
func (a *AESCrypto) DecryptString(value []byte, keyID string) (string, error) {
|
|
b, err := a.Decrypt(value, keyID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !utf8.Valid(b) {
|
|
return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-hiCh0", "non-UTF-8 in decrypted string")
|
|
}
|
|
|
|
return string(b), nil
|
|
}
|
|
|
|
func (a *AESCrypto) EncryptionKeyID() string {
|
|
return a.encryptionKeyID
|
|
}
|
|
|
|
func (a *AESCrypto) DecryptionKeyIDs() []string {
|
|
return a.keyIDs
|
|
}
|
|
|
|
func (a *AESCrypto) encryptionKey() string {
|
|
return a.keys[a.encryptionKeyID]
|
|
}
|
|
|
|
func (a *AESCrypto) decryptionKey(keyID string) (string, error) {
|
|
key, ok := a.keys[keyID]
|
|
if !ok {
|
|
return "", zerrors.ThrowNotFound(nil, "CRYPT-nkj1s", "unknown key id")
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func EncryptAESString(data string, key string) (string, error) {
|
|
encrypted, err := EncryptAES([]byte(data), key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(encrypted), nil
|
|
}
|
|
|
|
func EncryptAES(plainText []byte, key string) ([]byte, error) {
|
|
block, err := aes.NewCipher([]byte(key))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
maxSize := 64 * 1024 * 1024
|
|
if len(plainText) > maxSize {
|
|
return nil, zerrors.ThrowPreconditionFailedf(nil, "CRYPT-AGg4t3", "data too large, max bytes: %v", maxSize)
|
|
}
|
|
cipherText := make([]byte, aes.BlockSize+len(plainText))
|
|
iv := cipherText[:aes.BlockSize]
|
|
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stream := cipher.NewCFBEncrypter(block, iv)
|
|
stream.XORKeyStream(cipherText[aes.BlockSize:], plainText)
|
|
|
|
return cipherText, nil
|
|
}
|
|
|
|
func DecryptAESString(data string, key string) (string, error) {
|
|
text, err := base64.URLEncoding.DecodeString(data)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
decrypted, err := DecryptAES(text, key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(decrypted), nil
|
|
}
|
|
|
|
func DecryptAES(text []byte, key string) ([]byte, error) {
|
|
cipherText := make([]byte, len(text))
|
|
copy(cipherText, text)
|
|
|
|
block, err := aes.NewCipher([]byte(key))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(cipherText) < aes.BlockSize {
|
|
err = zerrors.ThrowPreconditionFailed(nil, "CRYPT-23kH1", "cipher text block too short")
|
|
return nil, err
|
|
}
|
|
iv := cipherText[:aes.BlockSize]
|
|
cipherText = cipherText[aes.BlockSize:]
|
|
|
|
stream := cipher.NewCFBDecrypter(block, iv)
|
|
stream.XORKeyStream(cipherText, cipherText)
|
|
|
|
return cipherText, err
|
|
}
|