cli/pkg/crypt/chachapoly/chachapoly.go
Vi dfd7c3ab2d feat(crypt): add LTHN, ChaCha20, RSA, PGP primitives (port from Enchantrix) (#346) (#354)
Co-authored-by: Claude <developers@lethean.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 20:30:28 +00:00

60 lines
1.9 KiB
Go

// Package chachapoly provides XChaCha20-Poly1305 authenticated encryption.
//
// Encrypt prepends a random nonce to the ciphertext; Decrypt extracts it.
// The key must be 32 bytes (256 bits).
//
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly).
package chachapoly
import (
"crypto/rand"
"fmt"
"io"
"golang.org/x/crypto/chacha20poly1305"
)
// Encrypt encrypts plaintext using XChaCha20-Poly1305.
// The key must be exactly 32 bytes. A random 24-byte nonce is generated
// and prepended to the returned ciphertext.
func Encrypt(plaintext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, fmt.Errorf("chachapoly: failed to create AEAD: %w", err)
}
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("chachapoly: failed to generate nonce: %w", err)
}
return aead.Seal(nonce, nonce, plaintext, nil), nil
}
// Decrypt decrypts ciphertext produced by Encrypt using XChaCha20-Poly1305.
// The key must be exactly 32 bytes. The nonce is extracted from the first
// 24 bytes of the ciphertext.
func Decrypt(ciphertext, key []byte) ([]byte, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, fmt.Errorf("chachapoly: failed to create AEAD: %w", err)
}
minLen := aead.NonceSize() + aead.Overhead()
if len(ciphertext) < minLen {
return nil, fmt.Errorf("chachapoly: ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen)
}
nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():]
decrypted, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("chachapoly: decryption failed: %w", err)
}
if len(decrypted) == 0 {
return []byte{}, nil
}
return decrypted, nil
}