benzcloud-server/internal/core/crypto/crypto.go

106 lines
3.0 KiB
Go

package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"golang.org/x/crypto/pbkdf2"
)
const (
// PBKDF2Iterations defines the mandatory 100,000 rounds of PBKDF2 key derivation.
PBKDF2Iterations = 100000
// KeyLength is 32 bytes for AES-256.
KeyLength = 32
// SaltLength is 16 bytes minimum.
SaltLength = 16
)
var (
ErrCiphertextTooShort = errors.New("ciphertext too short")
ErrDecryptionFailed = errors.New("decryption failed or corrupted data")
)
// DeriveKey derives a 32-byte AES-256 key from a passphrase and salt using PBKDF2 with 100,000 iterations.
func DeriveKey(passphrase string, salt []byte) []byte {
return pbkdf2.Key([]byte(passphrase), salt, PBKDF2Iterations, KeyLength, sha256.New)
}
// GenerateSalt creates a cryptographically secure random salt of the specified length.
func GenerateSalt(length int) ([]byte, error) {
if length < SaltLength {
length = SaltLength
}
salt := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, fmt.Errorf("failed to generate random salt: %w", err)
}
return salt, nil
}
// GenerateToken generates a cryptographically random 32-byte hex token.
func GenerateToken() (string, error) {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", fmt.Errorf("failed to generate token: %w", err)
}
return hex.EncodeToString(b), nil
}
// Encrypt encrypts plaintext using AES-256-GCM with a random 12-byte nonce.
// The output format is: [nonce (12 bytes)] + [ciphertext + 16-byte auth tag].
func Encrypt(key, plaintext []byte) ([]byte, error) {
if len(key) != KeyLength {
return nil, fmt.Errorf("invalid key length: must be %d bytes", KeyLength)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher block: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
// Decrypt decrypts AES-256-GCM ciphertext created by Encrypt.
func Decrypt(key, ciphertext []byte) ([]byte, error) {
if len(key) != KeyLength {
return nil, fmt.Errorf("invalid key length: must be %d bytes", KeyLength)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher block: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, ErrCiphertextTooShort
}
nonce, actualCiphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, actualCiphertext, nil)
if err != nil {
return nil, ErrDecryptionFailed
}
return plaintext, nil
}