1 Encryption-and-Hashing
Virgil edited this page 2026-02-19 16:59:30 +00:00

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

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