diff --git a/internal/cryptography/aes.go b/internal/cryptography/aes.go index e69de29..7aeef35 100644 --- a/internal/cryptography/aes.go +++ b/internal/cryptography/aes.go @@ -0,0 +1,208 @@ +package cryptography + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" +) + +func randomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := io.ReadFull(rand.Reader, b) + return b, err +} + +func normalizeKey(key []byte) ([]byte, error) { + switch len(key) { + case 16, 24, 32: + return key, nil + default: + return nil, errors.New("AES key length must be 16, 24, or 32 bytes") + } +} + +func AESGCMEncrypt(plaintext, key []byte) (string, error) { + key, err := normalizeKey(key) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce, err := randomBytes(gcm.NonceSize()) + if err != nil { + return "", err + } + + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + out := append(nonce, ciphertext...) + + return base64.RawURLEncoding.EncodeToString(out), nil +} + +func AESGCMDecrypt(encoded string, key []byte) ([]byte, error) { + key, err := normalizeKey(key) + if err != nil { + return nil, err + } + + data, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(data) < gcm.NonceSize() { + return nil, errors.New("ciphertext too short") + } + + nonce := data[:gcm.NonceSize()] + ciphertext := data[gcm.NonceSize():] + + return gcm.Open(nil, nonce, ciphertext, nil) +} + +func pkcs7Pad(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padtext...) +} + +func pkcs7Unpad(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, errors.New("invalid padding") + } + padding := int(data[length-1]) + if padding == 0 || padding > length { + return nil, errors.New("invalid padding") + } + return data[:length-padding], nil +} + +func AESCBCEncrypt(plaintext, key []byte) (string, error) { + key, err := normalizeKey(key) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + plaintext = pkcs7Pad(plaintext, block.BlockSize()) + + iv, err := randomBytes(block.BlockSize()) + if err != nil { + return "", err + } + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(plaintext, plaintext) + + out := append(iv, plaintext...) + return base64.RawURLEncoding.EncodeToString(out), nil +} + +func AESCBCDecrypt(encoded string, key []byte) ([]byte, error) { + key, err := normalizeKey(key) + if err != nil { + return nil, err + } + + data, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + if len(data) < block.BlockSize() { + return nil, errors.New("ciphertext too short") + } + + iv := data[:block.BlockSize()] + data = data[block.BlockSize():] + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(data, data) + + return pkcs7Unpad(data) +} + +func AESCFBEncrypt(plaintext, key []byte) (string, error) { + key, err := normalizeKey(key) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + iv, err := randomBytes(block.BlockSize()) + if err != nil { + return "", err + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(plaintext, plaintext) + + out := append(iv, plaintext...) + return base64.RawURLEncoding.EncodeToString(out), nil +} + +func AESCFBDecrypt(encoded string, key []byte) ([]byte, error) { + key, err := normalizeKey(key) + if err != nil { + return nil, err + } + + data, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + if len(data) < block.BlockSize() { + return nil, errors.New("ciphertext too short") + } + + iv := data[:block.BlockSize()] + data = data[block.BlockSize():] + + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(data, data) + + return data, nil +}