Crypto overview
What @sukujs/crypto offers and which tool fits which job.
@sukujs/crypto is a small set of security helpers built on the Web Crypto API. It has no
dependencies, runs on Node 22+, Bun and edge runtimes, and works with or without the rest of
Suku.
bun add @sukujs/cryptoThis package has not had an independent security audit. It implements no cryptography of its own: every operation is done by the platform, and the package adds safe defaults and input checking around it.
Importing
Use the default export, which carries every function:
import sukuCrypto from '@sukujs/crypto';
const hash = await sukuCrypto.hashPassword(password);
const token = sukuCrypto.randomToken();Each function is also a named export, which lets a bundler drop the ones you do not use:
import { hashPassword, randomToken } from '@sukujs/crypto';They are the same functions. These pages use the default export.
Choosing the right tool
Most mistakes come from using the wrong tool, not from using a tool badly.
| You need to | Use | Why |
|---|---|---|
| Let a client prove who it is on each request | JWT | Verified without a database lookup. |
| Let other services verify your tokens | JWT with a key pair | They hold only the public key. |
| Store a password | hashPassword | Slow on purpose, salted, upgradeable. |
| Send a value the client may read but not change | createSigner | Tamper-proof, still readable. |
| Send a value the client must not read | createSealer | Encrypted and tamper-proof, with expiry. |
| Create a session id, API key or reset token | randomToken() | 256 bits from the system's secure source. |
| Fingerprint a file or cache key | sha256() | Fast. Never for passwords. |
| Compare two secrets | timingSafeEqual() | Does not leak where they differ. |
Signed, sealed, or a JWT?
| Signed value | Sealed value | JWT | |
|---|---|---|---|
| Client can read it | Yes | No | Yes |
| Client can change it | No | No | No |
| Built-in expiry | No | Optional ttl | Yes, by default |
| Verifiable by another service | Only with the shared secret | Only with the shared secret | Yes, with a public key |
| Typical use | Cookie value, webhook, link | Encrypted cookie, OAuth state, reset link | API authentication |
A JWT's payload is only encoded, not encrypted. Anyone holding the token can read it.
Everything in the package
| Area | Functions |
|---|---|
| Tokens | createJwt, bearerToken, decodeJwtUnverified |
| Passwords | hashPassword, verifyPassword, needsRehash |
| Signed values | createSigner |
| Encrypted values | createSealer |
| Random | randomToken, randomBytes, randomHex, randomUUID, randomInt |
| Hashing | sha256, sha512, digest |
| Encoding and comparison | base64urlEncode, base64urlDecode, hexEncode, timingSafeEqual |
Secrets
Every helper that takes a secret requires at least 32 bytes and refuses anything shorter. Generate one per purpose and keep it in your environment.
import sukuCrypto from '@sukujs/crypto';
console.log(sukuCrypto.randomToken(32)); // run once, paste into your secret storeValidate them at startup with configuration, so a missing secret stops the app before it serves a request.
export const Env = defineConfig(
'env',
z.object({
JWT_SECRET: z.string().min(43),
COOKIE_SECRET: z.string().min(43),
}),
);Errors
Functions that verify something you control return a plain result: verifyPassword gives
true or false, and unpack and unseal give the value or undefined. They never throw
on bad input.
jwt.verify throws a JwtError with a code, because the reason matters for logging.
Whatever the code, respond to the client with a plain 401. See
best practices.
Not included
Encrypted JWTs (JWE), fetching keys from an identity provider (JWKS), OAuth and OpenID Connect flows, token revocation lists, and Argon2 or bcrypt.