NimCypher β’ Port of Monocypher in Nim + high-level API and extensions
nimble install nimcypher
NimCypher is a pure-Nim cryptographic library that started as a faithful port of
Monocypher 4.0.3 and has grown beyond it with the addition
of AES-128/192/256 block cipher and AES-GCM authenticated encryption, the SHA-2
family (SHA-256/384/512) plus HMAC-SHA-1 and HKDF-SHA-256, legacy MD5
and HMAC-MD5 for interop, and the non-cryptographic xxHash family (XXH32,
XXH64, XXH3_64, XXH128) for checksums, and asymmetric
primitives: RSA (PKCS#1 v1.5, PSS, OAEP) and ECDSA/ECDH over P-256/P-384/P-521 and
secp256k1. It has zero C dependency and no runtime dependencies beyond the Nim
standard library, so it is easy to deploy and easy to audit.
It ships two layers:
- A high-level, easy-to-remember API (
import nimcypher) for the common tasks: AES-GCM sealing, hashing, authenticated encryption and sealing, X25519 and ECDH key exchange, EdDSA and ECDSA signatures, RSA signing and encryption, and password hashing. - The low-level primitives (
nimcypher/algos/...) for fine-grained control, exposing the full surface with an idiomatic Nim style:openArray[byte]in,seq[byte]/array[N, byte]out, contexts as objects withinit/update/final, andOption/boolwhere an operation can fail.
Every primitive is cross-checked byte-for-byte against the reference C Monocypher implementation and the NIST test-vector suite.
Note
This is an ambitious, experimental community project written in pure Nim with heavy LLM assistance. APIs may still change, and security-critical uses deserve independent review. Contributions are welcome: bug reports, test vectors, benchmarks, and ports.
High-level API (import nimcypher):
- AES-GCM sealing with random nonces, plus AES block encryption in common modes
- Authenticated encryption and sealing with XChaCha20-Poly1305, including streaming
- Hashing with BLAKE2b and the SHA-2 family, HMAC variants, and HKDF key derivation
- Legacy
MD5/HMAC-MD5for interop andxxHash(XXH32/XXH64/XXH3) checksums - Argon2id password hashing, verification, and key derivation
- Key exchange with X25519 and ECDH over standard curves
- Signatures: EdDSA, deterministic ECDSA, and RSA PKCS#1 v1.5 plus PSS
- Asymmetric encryption with RSA-OAEP (legacy PKCS#1 v1.5 padding also available)
- RSA and elliptic-curve key generation, secret wiping, constant-time comparison, hex helpers
Low-level primitives (nimcypher/algos/...):
- AES block cipher and AES-GCM with streaming support, checked against NIST vectors
- BLAKE2b, SHA-2, HMAC, and HKDF building blocks
hashes/md5(RFC 1321, HMAC-MD5) andhashes/xxhash(XXH32/XXH64/XXH3_64/XXH128)- Argon2 password hashing in all three variants
- X25519 and ECDH key exchange, EdDSA and ECDSA signatures
- RSA signing, encryption, and key generation on an internal Montgomery engine
- BigInt helpers: byte conversion, primality testing, and random prime generation
- ChaCha20 stream ciphers, Poly1305, and Elligator 2 mappings
AES-256-GCM for authenticated encryption with random nonces:
import nimcypher/aes
import nimcypher/utils
let key = randomBytes[32]()
let sealed = gcmSeal(toBytes("attack at dawn"), key)
let plaintext = gcmOpen(sealed, key)
assert plaintext == toBytes("attack at dawn")import nimcypher/aes
import nimcypher/utils
let key = randomBytes[16]()
let iv = randomBytes[16]()
let ct = aesCbcEncrypt(key, iv, toBytes("secret message"))
let pt = aesCbcDecrypt(key, iv, ct)
assert pt == toBytes("secret message")
let ctr = aesCtrCrypt(key, iv, toBytes("stream mode"))
let pt2 = aesCtrCrypt(key, iv, ctr)
assert pt2 == toBytes("stream mode")Argon2id password hashing for storage, verification, and key derivation:
import nimcypher/password
import nimcypher/utils
let stored = hashPassword("hunter2")
assert verifyPassword("hunter2", stored)
let salt = generateSalt()
let key = deriveKeyFromPassword("hunter2", salt) # Secret[Key32]
assert key.data.len == 32Authenticated encryption with XChaCha20-Poly1305. Two parties derive the same shared secret from their passwords and exchange sealed messages:
import nimcypher/encrypt
import nimcypher/password
import nimcypher/utils
let (aliceSK, alicePK) = keyPairFromPassword("alice-passphrase", generateSalt())
let (bobSK, bobPK) = keyPairFromPassword("bob-passphrase", generateSalt())
assert alicePK != bobPK
let aliceShared = sharedSecret(aliceSK.data, bobPK)
let bobShared = sharedSecret(bobSK.data, alicePK)
assert aliceShared == bobShared
let msg = "Hi Bob, this is Alice."
let sealed = seal(msg, aliceShared.data)
let opened = unseal(sealed, bobShared.data)
assert opened == toBytes(msg)decrypt / unseal / aeadStreamRead raise ValueError when the MAC does not
verify, so a failed authentication never yields plaintext.
Encrypt and decrypt a message in chunks with the streaming AEAD API:
import nimcypher/encrypt
import nimcypher/utils
let key = randomBytes[32]()
let nonce = randomBytes[24]()
let message = "Hello, this is a test of AEAD streaming!"
var stream = aeadStreamInitX(key, nonce) # also initDjb / initIetf
let (cipher1, mac1) = aeadStreamWrite(stream, toBytes(message[0 ..< 16]))
let (cipher2, mac2) = aeadStreamWrite(stream, toBytes(message[16 ..^ 1]))
var decStream = aeadStreamInitX(key, nonce)
let plain1 = aeadStreamRead(decStream, cipher1, mac1)
let plain2 = aeadStreamRead(decStream, cipher2, mac2)
assert plain1 & plain2 == toBytes(message)import nimcypher/hash
import nimcypher/utils
let digest = blake(toBytes("hello world")) # 32-byte BLAKE2b digest
let mac = blakeKeyed(toBytes("msg"), toBytes("key")) # keyed (MAC)
let sha = sha512Hex("hello world") # hex string
let sha2 = sha256Hex("hello world") # 32-byte SHA-256
let sha4 = sha384Hex("hello world") # 48-byte SHA-384
let hmac = sha512Hmac(toBytes("key"), toBytes("msg"))
let hmac2 = sha256Hmac(toBytes("key"), toBytes("msg")) # 32-byte HMAC-SHA-256
let hmac1 = sha1Hmac(toBytes("key"), toBytes("msg")) # 20-byte HMAC-SHA-1 (RFC 2202)
let okm = hkdfSha512(toBytes("ikm"), @[], toBytes("info"), 32)
let okm2 = hkdfSha256(toBytes("ikm"), @[], toBytes("info"), 32)
assert okm.len == 32 and okm2.len == 32
var st = initSha256() # streaming SHA-256 (also initSha512/initSha384)
st.update(toBytes("hello "))
st.update(toBytes("world"))
assert st.finish() == sha256(toBytes("hello world"))Legacy MD5 (broken, interop only) and non-cryptographic xxHash
(checksums only) live in the same module, with streaming support and
low-level access under nimcypher/hashes/md5 and nimcypher/hashes/xxhash:
import nimcypher/hash
import nimcypher/utils
assert md5Hex("abc") == "900150983CD24FB0D6963F7D28E17F72"
assert md5HmacHex("Jefe", "what do ya want for nothing?") ==
"750C783E6AB0B503EAA86E310A5DB738"
var m = initMd5() # streaming MD5 (also initMd5Hmac)
m.update(toBytes("a"))
m.update(toBytes("bc"))
assert m.finishHex() == md5Hex("abc")
assert xxh32Hex("abc") == "32D153FF" # seeded 32-bit checksum
assert xxh64Hex("abc") == "44BC2CF5AD770999" # seeded 64-bit checksum
assert xxh3_64bitsHex("abc") == "78AF5F94892F3950"
assert xxh128Hex("abc") == "06B05AB6733A618578AF5F94892F3950"
var x = initXxh3_64() # streaming (also initXxh32/initXxh64/initXxh3_128)
x.update(toBytes("hello "))
x.update(toBytes("world"))
assert x.finish() == xxh3_64bits(toBytes("hello world"))import nimcypher/encrypt
import nimcypher/utils
let (aliceSk, alicePk) = x25519KeyPair(randomBytes[32]())
let (bobSk, bobPk) = x25519KeyPair(randomBytes[32]())
let shared = sharedSecret(aliceSk, bobPk)
assert shared == sharedSecret(bobSk, alicePk)import nimcypher/sign
import nimcypher/utils
let kp = generateSigningKeyPair(randomBytes[32]())
let sig = sign(kp.secretKey, toBytes("message"))
assert verify(kp.publicKey, toBytes("message"), sig)Deterministic RFC 6979 signatures in JWS R || S format; ECDH yields the
x-coordinate as coordLen bytes:
import nimcypher/ecdsa
import nimcypher/utils
var (alicePriv, alicePub) = generateEcKeyPair(P256)
var (bobPriv, bobPub) = generateEcKeyPair(P256)
assert ecValidatePublicKey(alicePub) and ecValidatePublicKey(bobPub)
let sig = ecdsaSign(alicePriv, toBytes("message")) # deterministic
assert ecdsaVerify(alicePub, toBytes("message"), sig)
assert ecdhSharedSecret(alicePriv, bobPub) == ecdhSharedSecret(bobPriv, alicePub)
wipeEcKey(alicePriv)
wipeEcKey(bobPriv)PKCS#1 v1.5 (RS256/384/512), PSS with salt = hash len (PS256/384/512),
OAEP-SHA-1/SHA-256 and legacy PKCS1-v1_5 encryption. Keys >= 2048 bits
for production; smaller sizes exist for tests only and are slow to generate
in pure Nim:
import nimcypher/rsa
import nimcypher/utils
var priv = generateRsaKeyPair(2048)
let pub = rsaPublicKey(priv)
let msg = toBytes("hello JOSE")
let sig = rsaPkcs1v15Sign(priv, rhSha256, msg) # RS256; also rsaPssSign
assert rsaPkcs1v15Verify(pub, rhSha256, msg, sig)
let ct = rsaOaepEncrypt(pub, rhSha256, msg) # RSA-OAEP-256
assert rsaOaepDecrypt(priv, rhSha256, ct) == msg
wipeRsaKey(priv)Indicative figures from tests/bench_rsa.nim on an x86_64 laptop
(-d:danger; results vary a few percent run to run). The default build is pure
Nim; the nimsimd build switches the Montgomery engine to 64-bit limbs with
MULX (needs BMI2, see
Optional SIMD acceleration):
| operation | pure Nim | with nimsimd |
|---|---|---|
| RSA-1024 sign | ~1.5ms | ~1.3ms |
| RSA-1024 verify (e = 65537) | ~0.2ms | ~0.2ms |
| RSA-2048 sign | ~6.3ms | ~4.9ms |
| RSA-2048 verify (e = 65537) | ~0.7ms | ~0.7ms |
| RSA-1024 keygen | ~30ms | ~30ms |
| RSA-2048 keygen | ~1s | ~1s |
For reference, RSA-2048 signing took about 333ms with the generic big-integer
power before the internal Montgomery sliding-window engine landed (roughly 50x
faster in pure Nim, roughly 65x with the MULX tier). One RSA-2048 sign splits
into about 3.9ms for the two CRT exponentiations plus 1.4ms for blinding
(2.5ms plus 1.3ms with nimsimd).
import nimcypher/utils
let key = randomBytes[32]()
let salt = generateSalt(16)
assert toHex(key).len == 64
assert constantTimeEqual(toBytes("abc"), toBytes("abc"))
var secret = @[byte 1, 2, 3]
wipe(secret)
assert secret == @[byte 0, 0, 0]For full control over every primitive (different Argon2 variants, Ed25519, Elligator, raw ChaCha20, Poly1305, RSA/ECDSA parameters, the EdDSA building blocks, or the streaming ChaCha20 extension), import the low-level modules:
import nimcypher/algos/x25519
import nimcypher/algos/ed25519
import nimcypher/algos/elligator
import nimcypher/algos/chacha20
import nimcypher/algos/rsa
import nimcypher/algos/ecdsa
let pk = x25519PublicKey(sk)
let (sk25519, pk25519) = ed25519KeyPair(seed)
let curve = elligatorMap(hidden)
var chachaCtx: Chacha20Context # streaming ChaCha20 (NimCypher extension)
initChacha20X(chachaCtx, key, nonce24)
var cipher = chacha20Encrypt(chachaCtx, toBytes("stream me"))
cipher.add chacha20Final(chachaCtx)See the test suite (tests/) for a complete walk-through of both layers.
nimble bench compares the pure-Nim port against the C Monocypher library
(installed system-wide and called through the FFI test bindings) and
nimcrypto (nimcrypto >= 0.7.3,
installed via nimble).
All sides are compiled with -d:danger --opt:speed
(the port with --mm:arc, -d:features.nimcypher.nimsimd. The Monocypher ratios are
Monocypher time / NimCypher time: below 1 means Monocypher is faster, above 1 means
NimCypher is faster. The NimCypher+SIMD column shows the SIMD-accelerated kernels
(AES-NI, PCLMULQDQ, AVX2); - means the primitive has no SIMD kernel.
The nimcrypto column shows the same workloads through nimcrypto (HW-accelerated
where available via SHA-NI/AVX/AES-NI); Nc/Nim and Nc/SIMD are
nimcrypto / NimCypher and nimcrypto / SIMD. Results vary a few percent run to run.
| operation | iters | MCypher | NCypher | +SIMD | NCrypto | M/Nim | M/SIMD | Nc/Nim | Nc/SIMD |
|---|---|---|---|---|---|---|---|---|---|
| blake2b 64B | 100000 | 0.0155s | 0.0182s | - | 0.0412s | 0.85x | - | 2.26x | - |
| blake2b 1024B | 20000 | 0.0196s | 0.0256s | - | 0.0602s | 0.77x | - | 2.35x | - |
| blake2b 65536B | 2000 | 0.1192s | 0.1611s | - | 0.3664s | 0.74x | - | 2.27x | - |
| blake2b 4x 1024B | 5000 | 0.0207s | 0.0277s | 0.0169s | - | 0.75x | 1.23x | - | - |
| blake2b 4x 65536B | 200 | 0.0473s | 0.0620s | 0.0345s | - | 0.76x | 1.37x | - | - |
| sha512 64B | 50000 | 0.0167s | 0.0152s | - | 0.0126s | 1.10x | - | 0.83x | - |
| sha512 1024B | 20000 | 0.0500s | 0.0549s | - | 0.0378s | 0.91x | - | 0.69x | - |
| sha512 65536B | 1000 | 0.1444s | 0.1557s | - | 0.1065s | 0.93x | - | 0.68x | - |
| chacha20 64B | 50000 | 0.0060s | 0.0076s | 0.0077s | - | 0.79x | 0.77x | - | - |
| chacha20 1024B | 20000 | 0.0308s | 0.0394s | 0.0304s | - | 0.78x | 1.01x | - | - |
| chacha20 65536B | 1000 | 0.0961s | 0.1223s | 0.0958s | - | 0.79x | 1.00x | - | - |
| poly1305 1024B | 50000 | 0.0257s | 0.0296s | - | - | 0.87x | - | - | - |
| poly1305 65536B | 2000 | 0.0627s | 0.0703s | - | - | 0.89x | - | - | - |
| aead lock+unlock 1024B | 10000 | 0.0457s | 0.0556s | 0.0467s | - | 0.82x | 0.98x | - | - |
| aead lock+unlock 65536B | 500 | 0.1275s | 0.1571s | 0.1328s | - | 0.81x | 0.96x | - | - |
| x25519 | 2000 | 0.1580s | 0.1549s | - | - | 1.02x | - | - | - |
| eddsa sign 1KB | 1000 | 0.0417s | 0.0400s | - | - | 1.04x | - | - | - |
| eddsa check 1KB | 1000 | 0.1189s | 0.1179s | - | - | 1.01x | - | - | - |
| ed25519 sign 1KB | 1000 | 0.0439s | 0.0423s | - | - | 1.04x | - | - | - |
| ed25519 check 1KB | 1000 | 0.1213s | 0.1200s | - | - | 1.01x | - | - | - |
| elligator map | 3000 | 0.0227s | 0.0203s | - | - | 1.12x | - | - | - |
| elligator rev | 3000 | 0.0221s | 0.0202s | - | - | 1.09x | - | - | - |
| argon2i 8blk 1pass | 20 | 0.0003s | 0.0005s | - | - | 0.62x | - | - | - |
| aes-ctr 1024B | 20000 | - | 0.1500s | 0.0141s | 0.5010s | - | 10.61x | 3.34x | 35.44x |
| aes-ctr 65536B | 1000 | - | 0.4818s | 0.0443s | 1.5668s | - | 10.88x | 3.25x | 35.37x |
| aes-gcm lock+unlock 1024B | 5000 | - | 0.0876s | 0.0612s | 0.2855s | - | 1.43x | 3.26x | 4.67x |
| aes-gcm lock+unlock 65536B | 300 | - | 0.2868s | 0.1824s | 1.0398s | - | 1.57x | 3.62x | 5.70x |
The port matches or slightly beats C for SHA-512, X25519, EdDSA/Ed25519 and Elligator.
On the symmetric primitives the scalar port is roughly 0.75-0.90x vs C Monocypher; with
the SIMD kernels, ChaCha20 reaches parity, AES-CTR gets a ~10x boost from AES-NI,
and AES-GCM reaches ~1.5x over the scalar path. Compared to nimcrypto (same machine,
same compiler flags), NimCypher is 2.3x faster on BLAKE2b, ~0.7x on SHA-512
(nimcrypto benefits from SHA-NI), and 3.3x (scalar) / 35x (SIMD) faster on AES-CTR
and 3.3x / 4.7x on AES-GCM.
The AES scalars are constant-time bitsliced implementations verified against the NIST test vectors; with AES-NI enabled, AES-GCM performance matches or beats the C Monocypher AES-NI path.
The blake2b 4x rows hash four messages at once with blake2bParallel (the C and
scalar-Nim columns run four one-shot hashes for the same work); the SIMD kernel brings
batched BLAKE2b to ~1.3-1.4x C.
nimble test
Runs three suites:
- Vector tests: Monocypher's own deterministic test vectors (RFC and known-answer
vectors, over 16,000 of them), ported into
tests/vectors.nim, covering every primitive. - Interop tests: byte-for-byte cross-checks between the pure-Nim port and the real C Monocypher library: key exchange, signatures, AEAD encryption/decryption, streaming, hashing, Argon2, Elligator, and constant-time verification.
- OpenSSL interop tests (
topenssl.nim): live two-way cross-checks against the systemopensslCLI over freshly generated keys: RSA PKCS#1 v1.5 / PSS / OAEP, ECDSA (P-256, P-384, secp256k1), AES-ECB/CBC/CTR, SHA digests, MD5 digests, HMAC-SHA-256, HMAC-MD5, IETF ChaCha20, X25519, and Ed25519. AES-GCM is covered by NIST vectors intgcminstead (openssl encrejects AEAD ciphers). - High-level tests: round trips and error handling for the
import nimcypherAPI (thighlevel.nim), cross-checked against the low-level primitives.
The interop tests require a system-installed C Monocypher discoverable via pkg-config
(headers in the include path, libmonocypher.a linkable). They use FFI bindings copied
from the openpeeps/e2ee package (see tests/monocypher_ffi.nim).
The OpenSSL suite shells out to the openssl binary at test time. If none is found it
passes trivially with a skip note; set NIMCYPHER_REQUIRE_OPENSSL=1 (as CI does) to
fail loudly instead. On macOS, point PATH at the Homebrew OpenSSL 3 first: the system
LibreSSL lacks some pkeyutl options the suite needs.
nimble bench
nimble bench installs nimsimd, builds the suite with -d:features.nimcypher.nimsimd
and prints a single Markdown table with both the scalar reference (NimCypher) and the
SIMD-accelerated (NimCypher+SIMD) columns side by side (see
Benchmarks). The scalar-only baseline is obtained by compiling
tests/tbench.nim directly without the feature flag.
NimCypher ships optional SIMD-accelerated kernels behind the nimsimd feature flag.
They are off by default (the library stays zero-dependency and runs on any CPU)
and are selected with the Nimble nimsimd feature:
nimble install nimsimd # install the dependency
nim c -d:features.nimcypher.nimsimd app.nim # enable at build time
Consumers enable it from their own *.nimble file instead:
requires "nimcypher >= 0.1.0[nimsimd]"
Requirements and what gets accelerated:
- amd64: AES-NI (
-maes) and PCLMULQDQ (-mpclmul) for AES block cipher and GHASH, AVX2 (-mavx2) for ChaCha20 and batched BLAKE2b. - arm64: ARMv8 Crypto Extensions (
+crypto) for AES block cipher, NEON for ChaCha20 and batched BLAKE2b. GHASH uses the scalar CT reference path on ARM (PMULL integration planned). - Accelerates AES-128/192/256 (8 blocks in parallel on amd64 via AES-NI, 4 blocks on
arm64 via ARMv8 AESE/AESMC) and therefore all AES-GCM encryption/decryption
(AES-NI + PCLMULQDQ on amd64). Also accelerates ChaCha20 (
chacha20Djb/Ietf/X, HChaCha20) and the ChaCha20 half of AEAD. It also accelerates batched BLAKE2b throughblake2bParallel, which hashes four messages at once with one SIMD lane each. - Accelerates RSA on amd64 via 64-bit-limb Montgomery arithmetic with MULX
(BMI2,
-mbmi2): thenimsimdbinary requires BMI2 (Intel Haswell / AMD Excavator and newer). RSA-2048 sign drops from ~6.3ms to ~4.9ms (tests/bench_rsa.nim); other architectures keep the portable 32-bit path.
On x86_64 the two-block AVX2 kernel for ChaCha20 roughly reaches parity with C Monocypher on the full one-shot, while AES-NI gives a ~10x improvement over the bitsliced scalar core on bulk operations (CTR/ECB). The GCM construction benefits from both AES-NI and PCLMULQDQ, reaching ~1.5x over the scalar implementation.
The scalar constant-time bitsliced AES core always stays available as the reference path
and is cross-checked byte-for-byte by the test suite (nimble test_simd runs ChaCha20,
AEAD, BLAKE2b, AES, GCM, Montgomery/RSA and interop tests with the feature enabled).
The portable 32-bit Montgomery path likewise stays the RSA default; the MULX tier is
differential-tested against it (tests/tmontgomery.nim, all bit sizes to 4096).
NimCypher is a faithful port of Monocypher 4.0.3 (monocypher.c and the optional
monocypher-ed25519.c). It is verified in two independent ways:
- It passes Monocypher's own deterministic test-vector suite, ported into Nim.
- It produces byte-identical output to the C Monocypher library across all primitives (key exchange, signatures, AEAD, hashing, Argon2, Elligator), including cross signing/verifying between the two implementations.
Constant-time properties are preserved: the field arithmetic uses the same carry chains
and bit tricks as the reference C code, secret-dependent comparisons go through
constantTimeEqual, and wipe uses a compiler barrier so it is never optimized away.
- The low-level primitives have no random number generator: provide keys, nonces and
seeds yourself. The high-level API's
randomBytes/generateSaltuseurandom. - Never reuse a ChaCha20 nonce with the same key. The XChaCha20 AEAD nonce is 192 bits,
so random nonces (
seal) are safe in practice. - For AES-GCM, use a unique 96-bit nonce per message under a given key.
gcmSealgenerates a random nonce; never reuse nonce+key. decrypt/unseal/aeadStreamRead/aesGcmDecryptverify the MAC in constant time and never return plaintext on failure; they raiseValueErrorinstead.- The AES scalar core is constant-time (bitsliced, no lookup tables); AES-NI and
PCLMULQDQ are hardware constant-time by design. The HW path is only activated behind
the
nimsimdfeature flag. - RSA private operations use CRT + RSA blinding to mitigate the variable-time
powmod(pkg/bigints); OAEP/PKCS1-v1_5 decoders use a singledecryption errormessage. Prefer OAEP over PKCS1-v1_5 for new uses. Use keys>= 2048bits. - ECDSA scalar multiplication is a variable-time double-and-add (affine/Jacobian
coordinates with
invmod); signatures use deterministic RFC 6979 nonces (no RNG failure mode), but do not rely on timing side-channel resistance for ECDSA/RSA private ops in hostile shared-CPU environments. - RSA private ops run on an internal Montgomery sliding-window exponentiation
(pure Nim,
algos/internal/montgomery, blinding inverse via binary GCD): ~6.3ms/sign and ~1s 2048-bit keygen on a laptop (tests/bench_rsa.nim), ~50x faster than the genericpkg/bigintspowmod; thenimsimdbuild switches to 64-bit MULX limbs (internal/montgomery64, ~4.9ms/sign, needs BMI2).wipeRsaKey/wipeEcKeydrop BigInt references (GC frees the limbs); ephemeralseq[byte]buffers are scrubbed viawipe. - Use
constantTimeEqual, not==, to compare secrets. - Wipe secrets with
wipeonce you are done with them. MD5is broken (practical collisions) andHMAC-MD5is legacy only: both exist for interop with old formats, never for new designs.xxHash(XXH32/XXH64/XXH3) is not cryptographic: checksums and hash tables only, never signatures, MACs, or password hashing.
Direction, not a commitment. Items land as pure-Nim, tested against reference vectors and OpenSSL interop before they are documented here.
Next (asymmetric usability):
- RSA/EC key import and export: PKCS#1 / PKCS#8 / SEC1 DER + PEM, JWK
(
n,e,d,x,y,crv) for the supported RSA and EC curves. - JWS signing/verification and JWE decryption using the existing RS/PS/ES and RSA-OAEP primitives (no new crypto, mostly encoding).
- HKDF-SHA-384 in the high-level
hashAPI (low-level HMAC-SHA-384 inalgos/sha384already exists; only the HKDF expand/extract glue is missing).
Later (further algos):
- SHA-3 / Keccak and HMAC-SHA3 (FIPS 202) alongside the SHA-2 family.
- AES Key Wrap (KW/KWP, NIST SP 800-38F) for JOSE key management.
- PBKDF2-HMAC-SHA-2 for legacy password-based key derivation (Argon2id stays the default).
- TLS 1.3
HKDF-Expand-Labeland a minimal HPKE (DHKEM + AEAD) built from the existing ECDH/HKDF/AEAD pieces.
Hardening (no new algos):
- Constant-time EC field arithmetic to replace the current variable-time
double-and-add (ECDSA) and blinded
powmod(RSA) paths. - SHA-256/SHA-512 hardware kernels (SHA-NI) behind the existing
nimsimdfeature flag, mirroring the AES-NI/ChaCha20-AVX2 approach. - Expanded
nimble benchrows for SHA-256/384, RSA and ECDSA vs OpenSSL/nimcrypto.
Out of scope: X.509 path validation / TLS stacks; import/export helpers only; bring your own ASN.1 profile or use a dedicated TLS library.
- π Found a bug? Create a new Issue
- π Wanna help? Fork it!
BSD-2-Clause OR CC0-1.0 license. NimCypher is a port of Monocypher in Nim.
Made by Humans from OpenPeeps.
Copyright Β© 2026 OpenPeeps & Contributors β All rights reserved.