Chapter 1 · Practice 2 of 4
Use a high-level library such as libsodium, Tink or your platform's vetted API
Why it matters
High-level libraries remove the decisions that cause most mistakes. Google's Tink describes itself in terms of primitives defined by the security properties they satisfy, and its AEAD interface does not let callers supply a nonce at all: a new one is generated for every encryption, which rules out nonce reuse by construction.
| Option | Good for | Notes |
|---|---|---|
| libsodium (and bindings such as PyNaCl, sodium-native, libsodium.js) | apps that need encryption, signatures, key exchange | small, opinionated API; XChaCha20-Poly1305, Ed25519, X25519 |
| Tink (Java, Go, Python, C++, Obj-C) | teams using cloud KMS | keysets, built-in rotation, KMS envelope encryption |
Platform APIs (Web Crypto, .NET, Java JCA, Go crypto) | when you must stay on the standard library | lower level: you choose modes and nonces yourself |
| Protocols (TLS, HPKE, age) | data in transit, file encryption, sealed messages | prefer these over hand-built schemes |
Low-level APIs are not wrong, but they leave you to choose the mode, nonce size, tag length and key format. If you use them, wrap them in one small internal module with tests and known answer vectors, and let the rest of the codebase call only that module.
Pin library versions, track their security advisories, and avoid unmaintained wrappers that bundle an old copy of the C library.
Example
# PyNaCl (libsodium): authenticated secret-key encryption in two calls
import nacl.secret, nacl.utils
key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) # 32 bytes from a CSPRNG
box = nacl.secret.SecretBox(key)
ciphertext = box.encrypt(b"card ending 4242") # random nonce chosen and prepended for you
plaintext = box.decrypt(ciphertext) # raises CryptoError if tampered with