package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "io" "golang.org/x/crypto/argon2" ) // Params holds Argon2id parameters and the salt used for key derivation. type Params struct { Salt []byte Time uint32 Memory uint32 Threads uint8 } // DefaultParams generates new Params with a random 16-byte salt and // reasonable Argon2id defaults. func DefaultParams() (*Params, error) { salt := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, salt); err != nil { return nil, fmt.Errorf("generate salt: %w", err) } return &Params{ Salt: salt, Time: 3, Memory: 64 * 1024, Threads: 4, }, nil } // DeriveKey derives a 32-byte AES-256 key from a passphrase using Argon2id. func DeriveKey(passphrase string, p *Params) []byte { return argon2.IDKey([]byte(passphrase), p.Salt, p.Time, p.Memory, p.Threads, 32) } // Encryptor provides AES-256-GCM encryption and decryption. type Encryptor struct { aead cipher.AEAD } // NewEncryptor creates an Encryptor from a 32-byte key. func NewEncryptor(key []byte) (*Encryptor, error) { if len(key) != 32 { return nil, errors.New("key must be 32 bytes") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("new cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("new gcm: %w", err) } return &Encryptor{aead: aead}, nil } // Encrypt encrypts plaintext. The returned ciphertext has the nonce prepended. func (e *Encryptor) Encrypt(plaintext []byte) ([]byte, error) { nonce := make([]byte, e.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("generate nonce: %w", err) } return e.aead.Seal(nonce, nonce, plaintext, nil), nil } // Decrypt decrypts ciphertext produced by Encrypt. func (e *Encryptor) Decrypt(ciphertext []byte) ([]byte, error) { nonceSize := e.aead.NonceSize() if len(ciphertext) < nonceSize { return nil, errors.New("ciphertext too short") } nonce, ct := ciphertext[:nonceSize], ciphertext[nonceSize:] plaintext, err := e.aead.Open(nil, nonce, ct, nil) if err != nil { return nil, fmt.Errorf("decrypt: %w", err) } return plaintext, nil } // EncryptString encrypts a string and returns base64-encoded ciphertext. func (e *Encryptor) EncryptString(plaintext string) (string, error) { ct, err := e.Encrypt([]byte(plaintext)) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(ct), nil } // DecryptString decodes base64 ciphertext and returns the decrypted string. func (e *Encryptor) DecryptString(encoded string) (string, error) { ct, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", fmt.Errorf("decode base64: %w", err) } pt, err := e.Decrypt(ct) if err != nil { return "", err } return string(pt), nil }