fncConvertGui/internal/crypto/crypto.go
2026-02-25 23:17:08 +01:00

108 lines
2.8 KiB
Go

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
}