This document describes what github.com/ubgo/crypt defends against, what it doesn't, and the design choices behind its security guarantees.
We assume an adversary who can:
- Read network traffic between services (i.e., is on the wire).
- Read or modify the database where ciphertexts are stored.
- Submit arbitrary inputs to the application.
- Make timing observations on responses.
We assume the adversary cannot:
- Read process memory of a running server (different threat — KMS would address it).
- Subvert the Go or Node standard library implementations of AES, HMAC, etc.
- Compromise the kernel-level CSPRNG (
/dev/urandomon Linux/macOS,BCryptGenRandomon Windows).
Anything sealed with Seal (or Sealer.Seal) is computationally indistinguishable from random to anyone without the key. AES-256 in GCM mode provides 256-bit security against brute-force attack — well beyond any feasible attack budget.
Anything sealed with Seal is bound to its key, nonce, and AAD via a 128-bit GCM authentication tag. Modifying any part of the ciphertext, key, or AAD before Open fails with ErrTampered. The tag has a 2^-128 false-positive rate for an attacker guessing tags blindly.
Binding ciphertext to a context (user ID, tenant ID, message type) using AAD prevents cross-context replay. A token issued for user A is not valid as user B if AAD is "user:<id>". See USAGE.md.
Verify(HMAC) useshmac.Equal(constant-time).ConstantTimeEqualwrapscrypto/subtle.ConstantTimeCompare.VerifyPasswordusessubtle.ConstantTimeCompareafter re-deriving the argon2 hash.
- AEAD operations require exactly 32 bytes. Other lengths fail with
ErrInvalidKeyrather than silently downgrading. - AES-CBC accepts 16/24/32 bytes (AES-128/192/256). Choose the size that matches the system you're interoperating with; AES-256 is the conservative default for new CBC use.
- AEAD: AES-256-GCM (NIST SP 800-38D)
- Password: argon2id (RFC 9106, OWASP-recommended)
- MAC: HMAC-SHA256 (RFC 2104)
- Random: OS CSPRNG via
crypto/rand
No deprecated algorithms ship in v1.0+:
- No MD5, no SHA-1
- No DES, no 3DES
- No ECB mode (ever)
- No raw RSA
If an attacker has read access to the process's memory, every key in memory is exposed. We do not zero buffers proactively; Go's GC will eventually reclaim them but the timing is non-deterministic.
If your threat model includes memory disclosure (e.g., Heartbleed-class vulnerabilities, side-channel attacks on shared infrastructure), use a Key Management Service (AWS KMS, GCP KMS, HashiCorp Vault) with envelope encryption. v2 will ship a KMS adapter; v1 does not.
We rely on Go's crypto/aes implementation. On modern Intel and AMD CPUs (post-2010), AES-NI provides hardware-implemented constant-time AES. On older or non-x86 CPUs, Go falls back to a software AES implementation that is not constant-time and may be vulnerable to cache-timing attacks if an attacker shares the CPU.
For high-security deployments on shared infrastructure, ChaCha20-Poly1305 (planned in v1.1) is preferred. Its software implementation is naturally constant-time.
AES-256 is post-quantum-secure for confidentiality. Grover's algorithm halves the effective key length to 128 bits, which remains computationally infeasible.
We do not currently address post-quantum signatures. When Go std lib stabilizes ML-KEM and ML-DSA, we will add support.
The package does not protect against:
- Logging plaintext passwords
- Storing keys in environment variables that get printed
- Sharing keys via unencrypted channels (Slack, email)
- Reusing the same key across environments (dev/staging/prod)
These are operational disciplines outside the library's scope.
Ciphertexts have a fixed overhead of 29 bytes plus the plaintext length. An attacker who sees a ciphertext can determine the plaintext's length within 1 byte. If your threat model requires hiding length (e.g., distinguishing "yes" from "no" answers), pad the plaintext to a fixed size before encryption.
The v0.x package had a package-level cipherKey mutable via LoadKey(string). This was removed in v1 because:
- Default key footgun. If
LoadKeywas never called, encryption used a public alphabet"abcdefghijklmnopqrstuvwxyz012345". Anyone reading the package source could decrypt production data. - Race conditions.
LoadKeycould be called concurrently with encryption, producing undefined behavior. - Hostile to testing. Tests had to monkey-patch the global, breaking when run in parallel.
In v1, Sealer requires a key in its constructor and validates immediately. There is no way to encrypt without an explicitly-supplied key.
Cryptographic agility: when an algorithm needs to be upgraded (a flaw is found in AES-GCM, a faster alternative arrives), we add a new version byte. Decoders explicitly enumerate which versions they accept. Old ciphertexts continue to work; new writes use the new algorithm.
The version byte costs 1 byte per ciphertext — cheap insurance.
| Concern | bcrypt | argon2id |
|---|---|---|
| Memory hardness | low | high |
| GPU resistance | weak | strong |
| Side-channel resistance | medium | strong (id variant) |
| 72-byte input limit | yes (silent truncation) | no |
| OWASP recommendation 2023+ | acceptable | preferred |
argon2id is the modern choice. bcrypt may be added in v1.1 as a compatibility option for migrating from bcrypt-using systems, but it is not a security recommendation.
Raw SHA-256 is vulnerable to length extension attacks: given H(secret || message), an attacker can compute H(secret || message || padding || extension) without knowing the secret. HMAC structurally prevents this.
The v0.x package emitted hex output. We preserve this exactly to ensure backward compat. New code should use AEAD (base64url) — the choice between hex and base64 is purely aesthetic for new code.
- URL-safe characters (
A-Z,a-z,0-9,-,_) — no escaping in URLs or HTTP headers. - No
=padding — no special handling in URL parsers. - Compact: 4 chars per 3 bytes vs hex's 2 chars per 1 byte.
- Decodes byte-identical via Go's
base64.RawURLEncodingand Node'sBuffer.from(s, "base64url").
We use GitHub Private Security Advisories. To report a vulnerability:
- Visit https://github.com/ubgo/crypt/security/advisories/new
- Describe the issue, reproduction, impact.
We aim to:
- Acknowledge within 48 hours
- Assess severity within 7 days
- Ship a patch for P0 issues within 7 days of confirmed reproduction
Please do not file public GitHub issues for security vulnerabilities.
crypt v1.0 has not been independently audited. The implementation is small (under 1000 lines of code excluding tests and examples) and uses only Go standard library and golang.org/x/crypto/argon2. Both have been independently audited.
We welcome external review. If you have audit experience and find issues, please report via the security advisory process.