tom_crypto 1.4.0
tom_crypto: ^1.4.0 copied to clipboard
Cryptographic utilities for secure authentication and data protection including JWT tokens, password hashing, and RSA encryption.
Tom Crypto #
Published on pub.dev as tom_crypto 1.4.0.
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 |
| Name the signing key | TomJwtConfiguration.keyId |
Written as the kid header, for verifiers holding several keys |
| Name the audience | audience: constructor argument |
Written as the aud claim |
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.
Secrets in memory — TomProtectedValue #
| Capability | API | Notes |
|---|---|---|
| Hold a secret | TomProtectedValue.of(secret) |
XOR-ed with a per-instance salt from TomSecureBytes |
| Read it | reveal() |
The one accessor — every read is a call a reader can find |
| Compare it | digest, == |
SHA-256 of the plaintext; two values of one secret are equal |
| Show it | toString(), TomProtectedValue.mask |
Always the mask, never the secret |
jsonEncode refuses a protected value rather than writing it. The class has
an entry point of its own, package:tom_crypto/protected_value.dart, which
needs only tom_basics, so a package holding secrets in memory need not link
the JWT and RSA libraries; the tom_crypto.dart barrel exports it too.
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
);
For asymmetric signing, pass the private key and an RS/ES algorithm, and name the key so a verifier holding several keys — during a rotation — can pick it. A token meant for one service names it as its audience:
final configuration = TomJwtConfiguration(
RSAPrivateKey.raw(mySigningKey), // from package:dart_jsonwebtoken
JWTAlgorithm.RS256,
myRsaPrivateKey,
myRsaPublicKey,
false,
)..keyId = 'signing-2026-10';
final jwtString = TomServerJwtToken(
{'userId': '123'},
audience: 'orders-api',
signingConfiguration: configuration,
).getJWT('my-auth-server'); // header carries kid, payload carries aud
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 (the barrel; protected_value.dart is a second entry point)
│
├── 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)
│
├── protected_value.dart TomProtectedValue → secure_bytes + tom_basics
│ (tomSha256Hex)
│
└── 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 |
TomProtectedValue |
A secret in memory, read only through reveal() |
The JWT layer and the protected value are the parts that reach into
tom_basics (for TomBaseException and tomLog, and for tomSha256Hex); 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.