Encryption and Hashing
Back to Home
The crypt package (forge.lthn.ai/core/go-crypt/crypt) provides symmetric encryption, password hashing, key derivation, HMAC, and checksum functions. All functions use well-established cryptographic primitives from the Go standard library and golang.org/x/crypto.
Symmetric Encryption
Two high-level encryption functions are provided. Both derive a 32-byte key from the passphrase using Argon2id, generate a random salt, and prepend it to the output so that decryption is self-contained.
ChaCha20-Poly1305 (Default)
func Encrypt(plaintext, passphrase []byte) ([]byte, error)
func Decrypt(ciphertext, passphrase []byte) ([]byte, error)
Wire format: salt (16 bytes) + nonce (24 bytes) + ciphertext + Poly1305 tag
Uses XChaCha20-Poly1305 (extended nonce variant) for authenticated encryption. This is the recommended default — it is fast on all platforms and does not require hardware AES support.
encrypted, err := crypt.Encrypt([]byte("hello world"), []byte("passphrase"))
if err != nil {
log.Fatal(err)
}
decrypted, err := crypt.Decrypt(encrypted, []byte("passphrase"))
if err != nil {
log.Fatal(err)
}
fmt.Println(string(decrypted)) // "hello world"
AES-256-GCM
func EncryptAES(plaintext, passphrase []byte) ([]byte, error)
func DecryptAES(ciphertext, passphrase []byte) ([]byte, error)
Wire format: salt (16 bytes) + nonce (12 bytes) + ciphertext + GCM tag
Uses AES-256-GCM for authenticated encryption. Prefer this when AES-NI hardware acceleration is available or when interoperability with AES-based systems is required.
encrypted, err := crypt.EncryptAES([]byte("hello world"), []byte("passphrase"))
if err != nil {
log.Fatal(err)
}
decrypted, err := crypt.DecryptAES(encrypted, []byte("passphrase"))
if err != nil {
log.Fatal(err)
}
Low-Level Primitives
For cases where you manage key derivation yourself, the raw AEAD functions accept a 32-byte key directly:
// ChaCha20-Poly1305
func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error)
func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error)
// AES-256-GCM
func AESGCMEncrypt(plaintext, key []byte) ([]byte, error)
func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error)
Both prepend a random nonce to the output. The key must be exactly 32 bytes.
key := make([]byte, 32) // Derive or generate a 32-byte key
crypto_rand.Read(key)
encrypted, err := crypt.ChaCha20Encrypt([]byte("data"), key)
decrypted, err := crypt.ChaCha20Decrypt(encrypted, key)
Standalone chachapoly Package
The crypt/chachapoly sub-package provides a minimal Encrypt/Decrypt pair with no dependencies on the core error framework:
import "forge.lthn.ai/core/go-crypt/crypt/chachapoly"
encrypted, err := chachapoly.Encrypt(plaintext, key)
decrypted, err := chachapoly.Decrypt(encrypted, key)
Password Hashing
Argon2id (Recommended)
func HashPassword(password string) (string, error)
func VerifyPassword(password, hash string) (bool, error)
Produces hashes in the standard PHC string format:
$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>
Default parameters:
| Parameter | Value |
|---|---|
| Memory | 64 MB (m=65536) |
| Iterations | 3 (t=3) |
| Parallelism | 4 threads (p=4) |
| Key length | 32 bytes |
| Salt length | 16 bytes (random) |
Verification uses crypto/subtle.ConstantTimeCompare to prevent timing attacks.
hash, err := crypt.HashPassword("my-password")
// "$argon2id$v=19$m=65536,t=3,p=4$..."
valid, err := crypt.VerifyPassword("my-password", hash)
// true
bcrypt
func HashBcrypt(password string, cost int) (string, error)
func VerifyBcrypt(password, hash string) (bool, error)
Standard bcrypt hashing for interoperability with systems that require it. Cost must be between bcrypt.MinCost and bcrypt.MaxCost.
hash, err := crypt.HashBcrypt("my-password", 12)
valid, err := crypt.VerifyBcrypt("my-password", hash)
Key Derivation Functions
Argon2id
func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte
Derives a key using Argon2id with the default parameters (64 MB memory, 3 iterations, 4 threads). The salt must be 16 bytes. Returns a key of the requested length.
salt := make([]byte, 16)
crypto_rand.Read(salt)
key := crypt.DeriveKey([]byte("passphrase"), salt, 32)
// key is 32 bytes, suitable for AES-256 or ChaCha20
scrypt
func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error)
Parameters: N=32768, r=8, p=1.
key, err := crypt.DeriveKeyScrypt([]byte("passphrase"), salt, 32)
HKDF-SHA256
func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error)
Derives a key using HKDF with SHA-256. The salt and info parameters are optional (may be nil). Use this for deriving multiple keys from a single shared secret.
masterKey := []byte("shared-secret")
encKey, err := crypt.HKDF(masterKey, nil, []byte("encryption"), 32)
macKey, err := crypt.HKDF(masterKey, nil, []byte("mac"), 32)
HMAC
func HMACSHA256(message, key []byte) []byte
func HMACSHA512(message, key []byte) []byte
func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool
Compute and verify HMAC tags. VerifyHMAC uses crypto/hmac.Equal for constant-time comparison.
import "crypto/sha256"
key := []byte("hmac-key")
message := []byte("data to authenticate")
mac := crypt.HMACSHA256(message, key)
valid := crypt.VerifyHMAC(message, key, mac, sha256.New)
// true
Checksums
// File checksums (hex-encoded)
func SHA256File(path string) (string, error)
func SHA512File(path string) (string, error)
// Data checksums (hex-encoded)
func SHA256Sum(data []byte) string
func SHA512Sum(data []byte) string
// Verify a downloaded file
checksum, err := crypt.SHA256File("/path/to/file.tar.gz")
fmt.Println(checksum) // "a1b2c3d4..."
// Hash in-memory data
hash := crypt.SHA256Sum([]byte("hello"))
Cryptographic Parameters Reference
| Parameter | Value | Used By |
|---|---|---|
| Argon2id memory | 64 MB | Encrypt, EncryptAES, HashPassword, DeriveKey |
| Argon2id iterations | 3 | All Argon2id functions |
| Argon2id parallelism | 4 threads | All Argon2id functions |
| Argon2id key length | 32 bytes | Encrypt, EncryptAES |
| Salt length | 16 bytes | All high-level encrypt/hash functions |
| XChaCha20 nonce | 24 bytes | Encrypt, ChaCha20Encrypt |
| AES-GCM nonce | 12 bytes | EncryptAES, AESGCMEncrypt |
| scrypt N | 32768 | DeriveKeyScrypt |
| scrypt r | 8 | DeriveKeyScrypt |
| scrypt p | 1 | DeriveKeyScrypt |
Additional Sub-Packages
RSA (crypt/rsa)
RSA-OAEP encryption with PKCS1/PKIX key serialisation:
import "forge.lthn.ai/core/go-crypt/crypt/rsa"
svc := rsa.NewService()
pubKey, privKey, err := svc.GenerateKeyPair(4096) // minimum 2048 bits
encrypted, err := svc.Encrypt(pubKey, []byte("data"), []byte("label"))
decrypted, err := svc.Decrypt(privKey, encrypted, []byte("label"))
LTHN Hash (crypt/lthn)
Deterministic quasi-salted hashing (RFC-0004). The salt is derived from the input itself by reversing and applying character substitutions, so no separate salt storage is needed. Suitable for content identifiers and cache keys — not for passwords.
import "forge.lthn.ai/core/go-crypt/crypt/lthn"
hash := lthn.Hash("hello") // Deterministic SHA-256 with derived salt
valid := lthn.Verify("hello", hash) // true
OpenPGP (crypt/pgp)
Full OpenPGP operations using ProtonMail's go-crypto library:
import "forge.lthn.ai/core/go-crypt/crypt/pgp"
kp, err := pgp.CreateKeyPair("Alice", "alice@example.com", "password")
encrypted, err := pgp.Encrypt([]byte("secret"), kp.PublicKey)
decrypted, err := pgp.Decrypt(encrypted, kp.PrivateKey, "password")
signature, err := pgp.Sign([]byte("data"), kp.PrivateKey, "password")
err = pgp.Verify([]byte("data"), signature, kp.PublicKey)
See Also
- Home — Package overview and quick start
- Authentication — OpenPGP challenge-response authentication
- Trust-Engine — Agent trust tiers and policy-based access control