Tom Crypto
Part of the Tom framework by al-the-bear. © 2024–2026 Peter Nicolai Alexis Kyaw — BSD-3-Clause, see LICENSE. Portions of the RSA key handling are adapted from public examples (see Attribution); those parts retain their original licences.
Cryptographic utilities for secure authentication and data protection including JWT tokens, password hashing, and RSA encryption.
Overview
tom_crypto collects the small set of cryptographic primitives the Tom
framework needs for authentication and confidentiality, wrapped in a
task-oriented API so callers do not have to assemble PointyCastle engines by
hand. It covers four jobs:
- Store passwords safely with Argon2 — the winner of the Password Hashing Competition — including salt generation and a self-describing parameter string so you can rotate cost factors without invalidating old hashes.
- Issue and verify JWTs with HMAC or RSA signing, plus an optional RSA-encrypted payload section for claims that must stay secret from the bearer.
- Encrypt and sign arbitrary bytes with RSA-OAEP and RSA-SHA-256, block-chunked so payloads larger than one RSA block just work.
- Generate and (de)serialise RSA keys to and from PEM (PKCS#1 and PKCS#8), with 2048-bit key generation backed by a Fortuna CSPRNG.
Everything is plain Dart (no Flutter dependency), runs on the server and in
command-line tools, and builds on pointycastle,
asn1lib, and
dart_jsonwebtoken.
Read the Security notes before shipping. This package gives you correct primitives, but using them safely (key storage, dummy-key replacement, cost tuning) is the caller's responsibility, and the defaults are tuned for development, not production secrets.
Installation
dependencies:
tom_crypto: ^1.1.0
or from the command line:
dart pub add tom_crypto
Requires the Dart SDK ^3.10.0 (records and patterns are used in the password
API). Pure Dart — works in server apps, CLI tools, and Flutter alike.
When you work directly with RSA key objects (RSAPublicKey, RSAPrivateKey,
AsymmetricKeyPair) you also import the types from PointyCastle, which
tom_crypto does not re-export:
import 'package:tom_crypto/tom_crypto.dart';
import 'package:pointycastle/export.dart';
Features
Password hashing — TomPasswordHasher
| Capability | API | Notes |
|---|---|---|
| Hash a password | hashPassword(password) → (hash, spec) |
Random per-password salt; returns a record |
| Verify a password | verifyPassword(password, hash, spec) |
Re-derives with the stored spec/salt |
| Generate a salt | generateSalt(length) |
Hex string over TomSecureBytes |
| Build a derivator | buildKeyDerivator([spec, salt]) |
Lower-level Argon2 access |
| Tune defaults | globalSettingDefaultHashSpec, globalSettingDefaultSaltLength |
Process-wide cost factors |
JWT tokens — TomServerJwtToken / TomClientJwtToken
| Capability | API | Notes |
|---|---|---|
| Issue a signed token | TomServerJwtToken(public, …).getJWT(issuer) |
HMAC (default) or RSA signing |
| Encrypt sensitive claims | encryptedData: constructor argument |
RSA-OAEP encrypted encrypted claim |
| Parse a token | TomClientJwtToken(jwt) |
Decodes claims, auto-decrypts secrets |
| Read public claims | .payload, .issuer, .subject, .audience, .jwtId |
Standard JWT accessors |
| Read decrypted claims | .secretData |
Populated when decrypt: true |
| Configure keys/algorithm | TomJwtConfiguration(...), defaultSignConfiguration |
Swap dummy keys for production keys |
RSA encryption & signatures — top-level functions
| Capability | API | Notes |
|---|---|---|
| Encrypt bytes | rsaEncrypt(publicKey, bytes) |
OAEP padding, block-chunked |
| Decrypt bytes | rsaDecrypt(privateKey, cipher) |
OAEP padding, block-chunked |
| Sign bytes | rsaSign(privateKey, bytes) |
SHA-256 digest |
| Verify a signature | rsaVerify(publicKey, data, sig) |
Returns false on tampered input |
RSA key management — RsaKeyHelper
| Capability | API | Notes |
|---|---|---|
| Seed a CSPRNG | getSecureRandom() |
Fortuna, seeded from TomSecureBytes |
| Generate a key pair | computeRSAKeyPair(random) |
2048-bit, exponent 65537 |
| Parse a public key | parsePublicKeyFromPem(pem) |
PKCS#1 and PKCS#8 auto-detected |
| Parse a private key | parsePrivateKeyFromPem(pem) |
PKCS#1 and PKCS#8 auto-detected |
| Encode a public key | encodePublicKeyToPemPKCS1(key) |
PEM output |
| Encode a private key | encodePrivateKeyToPemPKCS1(key) |
PEM output |
| Sign a string | sign(plainText, privateKey) |
Base64 SHA-256 signature |
Raw randomness — TomSecureBytes
| Capability | API | Notes |
|---|---|---|
| Draw secure bytes | TomSecureBytes.generate(length) |
Uniform over the full 0..255 range |
This is the package's single source of raw randomness — the Fortuna seed and
the password salt both draw from it. Use it directly when you need secret bytes
of your own (a TOTP shared secret, an API key) rather than reaching for
Random.secure() at the call site.
Quick start
Hash a password, then verify it — the single most common use of this package:
import 'package:tom_crypto/tom_crypto.dart';
void main() {
// Hash a new password. You get back the hash and the spec that produced it.
final (hash, spec) = TomPasswordHasher.hashPassword('correct horse battery');
print('spec : $spec');
print('hash : ${hash.substring(0, 24)}…'); // salt$hash, hex-encoded
// Store BOTH `hash` and `spec` in your database, then later:
final good = TomPasswordHasher.verifyPassword('correct horse battery', hash, spec);
final bad = TomPasswordHasher.verifyPassword('wrong password', hash, spec);
print('correct password valid? $good');
print('wrong password valid? $bad');
}
Output (the hash differs every run because the salt is random):
spec : Argon2;2i,13,4,65536,4,128
hash : 7f3c…$a91b…
correct password valid? true
wrong password valid? false
The spec string is self-describing
(Argon2;variant,version,iterations,memory,lanes,keyLength), so verification
needs nothing but the values you already stored.
Example projects
| Example | What it shows |
|---|---|
tom_crypto_sample |
The full article-grade walkthrough — password hashing, JWT issue/verify/encrypt, and RSA encryption/signing/keygen — as seven runnable, offline examples with inline expected output. |
| Quick start | Hash and verify a password |
| Password hashing | Storage format and cost tuning |
| JWT tokens | Issue, encrypt, parse, and read claims |
| RSA encryption | Generate keys, encrypt, decrypt |
| Digital signatures | Sign data and verify integrity |
| Working with PEM keys | Parse and encode PEM |
For a self-contained runnable project, see the
tom_crypto_sample; the snippets below are each copy-paste runnable too.
Usage
Password hashing
hashPassword returns a (hash, spec) record. Persist both. The hash is
salt$hash (both hex), and the spec carries every parameter verifyPassword
needs to re-derive the key — so you can change the global defaults later without
breaking existing accounts.
final (hash, spec) = TomPasswordHasher.hashPassword('userPassword123');
// user.passwordHash = hash;
// user.hashSpec = spec;
final ok = TomPasswordHasher.verifyPassword('userPassword123', hash, spec);
The default spec is Argon2;2i,13,4,65536,4,128 — Argon2i, version 1.3, 4
iterations, 64 MB of memory, 4 lanes, 128-byte output. To raise the cost for new
hashes (existing hashes keep verifying against their own stored spec):
// 6 iterations, 128 MB memory — slower, stronger.
TomPasswordHasher.globalSettingDefaultHashSpec = 'Argon2;2i,13,6,131072,4,128';
Tune these on the hardware that will run the verification so a login stays comfortably under your latency budget.
JWT tokens
The server issues a token; the client parses it. Public claims live in
the payload visible to anyone holding the token. Anything you pass via
encryptedData is RSA-encrypted into a single encrypted claim and only
recovers on a holder that owns the matching private key.
// --- Server side ---
final token = TomServerJwtToken(
{'userId': '123', 'role': 'admin'}, // public claims
encryptedData: {'sessionSecret': 'abc123'}, // RSA-encrypted claim
expiresIn: const Duration(hours: 24),
);
final jwtString = token.getJWT('my-auth-server');
// --- Client side ---
final parsed = TomClientJwtToken(jwtString);
print(parsed.issuer); // my-auth-server
print(parsed.payload?['userId']); // 123
print(parsed.secretData?['sessionSecret']); // abc123 (decrypted)
Skip decryption when you only need the public claims (and have no private key):
final parsed = TomClientJwtToken(jwtString, decrypt: false);
Signing and encryption keys come from a TomJwtConfiguration. The bundled
TomJwtConfiguration.defaultSignConfiguration uses development keys and
logs a warning every time it encrypts or decrypts. Replace it once, at startup,
with your real keys:
TomJwtConfiguration.defaultSignConfiguration = TomJwtConfiguration(
SecretKey(myHmacSecret), // from package:dart_jsonwebtoken
JWTAlgorithm.HS256,
myRsaPrivateKey,
myRsaPublicKey,
false, // isDummy = false → no warning
);
RSA encryption
Generate a 2048-bit key pair, then encrypt and decrypt bytes with OAEP padding. Inputs larger than one RSA block are chunked automatically.
import 'dart:convert';
import 'dart:typed_data';
import 'package:tom_crypto/tom_crypto.dart';
import 'package:pointycastle/export.dart';
Future<void> main() async {
final random = RsaKeyHelper.getSecureRandom();
final pair = await RsaKeyHelper.computeRSAKeyPair(random);
final publicKey = pair.publicKey as RSAPublicKey;
final privateKey = pair.privateKey as RSAPrivateKey;
final plaintext = Uint8List.fromList(utf8.encode('Secret message'));
final cipher = rsaEncrypt(publicKey, plaintext);
final recovered = rsaDecrypt(privateKey, cipher);
print(utf8.decode(recovered)); // Secret message
}
RSA is for small payloads (keys, tokens, short secrets). For bulk data, encrypt the data with a symmetric cipher and use RSA only to wrap the symmetric key.
Digital signatures
Sign bytes with the private key; verify with the public key. rsaVerify
returns false for tampered data rather than throwing.
final data = Uint8List.fromList(utf8.encode('Important message'));
final signature = rsaSign(privateKey, data);
final ok = rsaVerify(publicKey, data, signature); // true
final tampered = rsaVerify(publicKey, otherData, signature); // false
For a string convenience that returns a base64 signature, use
RsaKeyHelper.sign(plainText, privateKey).
Working with PEM keys
Parse keys from PEM (PKCS#1 or PKCS#8 — the format is auto-detected) and encode them back out:
final publicKey = RsaKeyHelper.parsePublicKeyFromPem(pemPublicString);
final privateKey = RsaKeyHelper.parsePrivateKeyFromPem(pemPrivateString);
final pemPublic = RsaKeyHelper.encodePublicKeyToPemPKCS1(publicKey);
final pemPrivate = RsaKeyHelper.encodePrivateKeyToPemPKCS1(privateKey);
Architecture
package:tom_crypto/tom_crypto.dart (single export surface)
│
├── password_hashing.dart TomPasswordHasher → Argon2 (pointycastle)
│
├── jwt_token.dart TomServerJwtToken → dart_jsonwebtoken
│ TomClientJwtToken + rsa_encryption
│ TomJwtConfiguration
│ TomJwtTokenException → tom_basics
│
├── rsa_encryption.dart rsaEncrypt / rsaDecrypt → pointycastle
│ rsaSign / rsaVerify (OAEP, RSA-SHA256)
│
├── rsa_tools.dart RsaKeyHelper → pointycastle + asn1lib
│ getRsaKeyPair (top-level) (key gen, PEM I/O)
│
└── secure_bytes.dart TomSecureBytes → dart:math
(Random.secure)
| Type / function | Role |
|---|---|
TomPasswordHasher |
Argon2 password hashing and verification |
TomServerJwtToken |
Issues signed (and optionally encrypted) JWTs |
TomClientJwtToken |
Decodes and decrypts JWTs, exposes claims |
TomJwtConfiguration |
Holds signing/encryption keys and algorithm |
TomJwtTokenException |
TomBaseException raised on JWT failures |
rsaEncrypt / rsaDecrypt |
RSA-OAEP byte encryption |
rsaSign / rsaVerify |
RSA-SHA-256 signatures |
RsaKeyHelper |
Key generation and PEM parse/encode |
TomSecureBytes |
The package's only draw of raw random bytes |
The JWT layer is the only part that reaches into tom_basics (for
TomBaseException and tomLog); the password and RSA layers depend only on
pointycastle and asn1lib.
secure_bytes.dart is the one internal edge in the diagram above: both
TomPasswordHasher.generateSalt and RsaKeyHelper.getSecureRandom route
through it rather than calling Random.secure() themselves. One implementation
means a bound cannot go stale in a copy that nobody is reading.
Security notes
This package provides correct primitives. Using them safely is on you — these are the caveats that matter most:
- Replace the development keys.
TomJwtConfiguration.defaultSignConfigurationships with hard-coded HMAC and RSA keys (flagged infalse_secrets:) purely so examples run. They are public — anyone can forge tokens against them. Set your own configuration withisDummy: falsebefore issuing real tokens. - Store the hash and the spec together.
verifyPasswordcannot work without the spec that produced the hash. Tune cost factors (globalSettingDefaultHashSpec) on production hardware; the defaults target development convenience, not a hostile attacker. - Don't log token contents in production.
TomClientJwtToken.toString()includes the full payload and decrypted secrets by default. SetTomClientJwtToken.globalSettingShowContentInToString = falsein production. - Public claims are not secret. Anything in
publicDatais base64 — readable by anyone holding the token. OnlyencryptedDatais confidential, and only while the RSA private key stays private. - Use RSA for small payloads. Encrypt bulk data with a symmetric cipher and wrap only the symmetric key with RSA. Keys are generated at 2048 bits with public exponent 65537 — the industry-standard minimum.
- Verify, then trust.
rsaVerifyreturnsfalse(it does not throw) on a modified signature; always check the boolean before acting on signed data.
Ecosystem
tom_crypto is one of the foundational packages under
tom_ai/basics/. It pairs naturally with:
- tom_basics — exceptions (
TomBaseException) and logging (tomLog) used by the JWT layer (direct dependency). - tom_basics_network — HTTP/transport helpers that carry the JWTs this package issues.
All tom_ai/basics/ packages share a single repository,
tom_basics.
Attribution
The RSA key generation and PEM parsing in rsa_tools.dart are adapted from the
public example at
flutter_rsa_generator_example;
those portions retain their original licensing. The remainder of the package is
BSD-3-Clause as in LICENSE.
Further documentation
- LICENSE — BSD-3-Clause licence text.
- Source library docs — every public type and function in
lib/src/carries dartdoc comments with usage examples. pointycastle,asn1lib,dart_jsonwebtoken— the underlying cryptographic libraries.
Status
Stable (1.1.0). Public API covers password hashing, JWT issuance/parsing, RSA
encryption/signatures, RSA key management, and secure random bytes.
dart analyze is clean. A
runnable, article-grade project lives in the
tom_crypto_sample.
Libraries
- tom_crypto
- Cryptographic utilities for secure authentication and data protection.