Reference · 10
Crypto and passkeys.
Hashing, signing, encryption, tokens and passkeys inside your methods.
nacl, jwt, sha256, base64url and webauthn are synchronous crypto APIs for deployed code. Import them from @flower-js/sdk/crypto or the SDK root. Apart from base64url, they only work inside Flower, not in Node or a browser.
NaCl
- Bytes in and out are
Uint8Array. Inputs are never modified.
- Key arguments also accept managed key handles.
- Wrong types or lengths throw. Failed authentication returns
null (or false for detached verify).
- Random functions need system entropy, which is only available in mutations. In queries, pass explicit seeds, keys and nonces.
| API | Contract |
|---|
nacl.randomBytes(length): Uint8Array | Random bytes. Mutations only (unless a custom PRNG is set). Length is capped by result budgets. |
|---|
nacl.setPRNG(source: NaClPRNG | null): void | Replace the random source for NaCl in this callback; null restores the default. You are responsible for its safety; a weak one can repeat keys or nonces. Doesn't affect JWT nonces. |
|---|
nacl.secretbox(message, nonce, key): Uint8Array | XSalsa20-Poly1305 encryption. 32-byte key, 24-byte nonce; output is a 16-byte tag plus ciphertext. Never reuse a nonce with the same key. |
|---|
nacl.secretbox.open(box, nonce, key): Uint8Array | null | Decrypt; null if tampered, wrong key, or too short. |
|---|
nacl.scalarMult(secret, publicKey): Uint8Array | X25519, 32-byte inputs and output. Returns all zeros for low-order peer points; your protocol must check for that. |
|---|
nacl.scalarMult.base(secret): Uint8Array | X25519 public key from a 32-byte secret. |
|---|
nacl.box(message, nonce, publicKey, secretKey): Uint8Array | Public-key encryption. 32-byte keys, 24-byte nonce, 16 bytes overhead. |
|---|
nacl.box.open(box, nonce, publicKey, secretKey): Uint8Array | null | Decrypt with the peer's public key and your secret key; null on failure. |
|---|
nacl.box.before(publicKey, secretKey): Uint8Array | SharedKey | Precompute a shared key: 32 bytes for raw keys, an opaque SharedKey for managed ones. Valid only in the current callback. |
|---|
nacl.box.after(message, nonce, sharedKey): Uint8Array | Same as secretbox with the shared key. |
|---|
nacl.box.open.after(box, nonce, sharedKey): Uint8Array | null | Same as secretbox.open. |
|---|
nacl.box.keyPair(): NaClKeyPair | Random X25519 key pair. Mutations only. |
|---|
nacl.box.keyPair.fromSecretKey(secret): NaClKeyPair | Key pair from a 32-byte secret. Works in queries. |
|---|
nacl.sign(message, secretKey): Uint8Array | Ed25519; returns 64-byte signature plus message. Secret key is 64 bytes (seed + public key). |
|---|
nacl.sign.open(signedMessage, publicKey): Uint8Array | null | Verify and return the message; null if invalid. 32-byte public key. |
|---|
nacl.sign.detached(message, secretKey): Uint8Array | 64-byte Ed25519 signature. |
|---|
nacl.sign.detached.verify(message, signature, publicKey): boolean | Verify a detached signature; false if invalid. Rejects weak keys and malleable signatures. |
|---|
nacl.sign.keyPair(): NaClKeyPair | Random Ed25519 key pair. Mutations only. |
|---|
nacl.sign.keyPair.fromSeed(seed): NaClKeyPair | Key pair from a 32-byte seed. Works in queries. |
|---|
nacl.sign.keyPair.fromSecretKey(secret): NaClKeyPair | Key pair from a 64-byte secret. Throws if its public half doesn't match the seed. |
|---|
nacl.hash(message): Uint8Array | SHA-512, 64 bytes. Not a password hash. |
|---|
nacl.verify(a, b): boolean | Constant-time equality. false for different lengths or two empty arrays. |
|---|
nacl.box.after.open(box, nonce, key): Uint8Array | null | Alias of secretbox.open. |
|---|
Every NaCl length constant
| API | Contract |
|---|
nacl.secretbox.keyLength | 32 bytes. |
|---|
nacl.secretbox.nonceLength | 24 bytes. |
|---|
nacl.secretbox.overheadLength | 16 bytes. |
|---|
nacl.scalarMult.scalarLength | 32 bytes. |
|---|
nacl.scalarMult.groupElementLength | 32 bytes. |
|---|
nacl.box.publicKeyLength | 32 bytes. |
|---|
nacl.box.secretKeyLength | 32 bytes. |
|---|
nacl.box.sharedKeyLength | 32 bytes. |
|---|
nacl.box.nonceLength | 24 bytes. |
|---|
nacl.box.overheadLength | 16 bytes. |
|---|
nacl.box.after.keyLength | 32 bytes. |
|---|
nacl.box.after.nonceLength | 24 bytes. |
|---|
nacl.box.after.overheadLength | 16 bytes. |
|---|
nacl.sign.publicKeyLength | 32 bytes. |
|---|
nacl.sign.secretKeyLength | 64 bytes. |
|---|
nacl.sign.seedLength | 32 bytes. |
|---|
nacl.sign.signatureLength | 64 bytes. |
|---|
nacl.hash.hashLength | 64 bytes. |
|---|
Formats and names match TweetNaCl, except Ed25519 is stricter (see sign.detached.verify and sign.keyPair.fromSecretKey). These are primitives: peer identity, key rotation and nonce management are up to you.
JWT
| API | Contract |
|---|
jwt.sign(claims, key, options): string | Sign a compact JWS with HS256 (32+ byte secret), RS256, ES256 or EdDSA. Deterministic; works in queries. Adds no exp or other claims for you. |
|---|
jwt.verify<Claims>(token, key, options): JWTVerified<Claims> | Check the signature against your key and allowed algorithms, then check standard claims. Returns claims and header; throws if invalid. |
|---|
jwt.encrypt(claims, key, options = {}): string | Compact JWE, dir + A256GCM only. 32-byte key or managed A256GCM handle. Omit the nonce only in a mutation; if you pass one, keep it unique. Anyone with the key can also create tokens. |
|---|
jwt.decrypt<Claims>(token, key, options = {}): JWTVerified<Claims> | Decrypt and check standard claims. Throws on any failure. |
|---|
| API | Contract |
|---|
NaClKeyPair | {publicKey: Uint8Array; secretKey: Uint8Array}, in separate buffers. |
|---|
NaClPRNG | (output: Uint8Array, length: number) => void. Fill exactly length bytes. If it throws, the output is zeroed and the error propagates. |
|---|
JWTAlgorithm | "HS256" | "RS256" | "ES256" | "EdDSA". ES256 is P-256; EdDSA is Ed25519. The token header never picks the algorithm. |
|---|
JWTKey | Uint8Array | string | ManagedKey. HMAC/AES keys are bytes; RSA/EC keys are PEM strings or DER bytes. NaCl raw keys are not valid here. |
|---|
JWTKeyFormat | "raw" | "pem" | "der". Defaults: pem for strings, raw for bytes; request der explicitly. PEM: PKCS#8, SPKI and PKCS#1. Certificates and SEC1 are not supported. |
|---|
JWTClaims | Readonly<Record<string, Json>>. exp, nbf and iat are in seconds, not milliseconds. |
|---|
JWTSignOptions | {algorithm; keyFormat?; kid?; typ?}. algorithm is required for raw keys; typ defaults to JWT. |
|---|
JWTValidationOptions | {issuer?; audience?: readonly string[]; subject?; clockToleranceSeconds?; requireExpiration?; typ?}. exp is required by default; tolerance defaults to 0. If the token has aud, you must pass audience. |
|---|
JWTVerifyOptions | JWTValidationOptions & {algorithms: readonly JWTAlgorithm[]; keyFormat?}. A required, nonempty allowlist; don't mix symmetric and asymmetric algorithms. |
|---|
JWTEncryptOptions | {nonce?: Uint8Array; kid?: string; typ?: string}. Nonce is 12 bytes and must be unique per key. With a managed key, Flower sets kid. |
|---|
JWTProtectedHeader | {alg: JWTAlgorithm | "dir"; enc?: "A256GCM"; kid?: string; typ?: string}. kid is informational; it doesn't select a key. |
|---|
JWTVerified | JWTVerified<Claims extends object = JWTClaims> = {claims: Claims; protectedHeader: JWTProtectedHeader}. The type parameter doesn't validate your custom claims. |
|---|
Claim checks:
exp is required unless requireExpiration: false, and is always checked when present.
nbf is checked when present. iat is type-checked only; there's no max age.
issuer, subject and typ must match exactly. At least one audience must match.
- Time is Flower's invocation clock. Queries that verify tokens re-run as time passes, so an expired token isn't accepted from cache.
- Only
alg, kid, typ (and enc) headers are allowed. No JWKS fetching and no unverified decode. Validate custom claims yourself.
SHA-256 and base64url
| API | Contract |
|---|
sha256(message): Uint8Array | The 32-byte SHA-256 digest of a Uint8Array. Deterministic; works in queries. nacl.hash is SHA-512. |
|---|
sha256.hashLength | 32. |
|---|
base64url.encode(bytes): string | Unpadded base64url, the form WebAuthn and JOSE give bytes in JSON. Plain JavaScript; works anywhere. |
|---|
base64url.decode(text): Uint8Array | Accepts optional padding. Throws TypeError for other alphabets, impossible lengths or nonzero unused bits, so each byte string has one accepted encoding. |
|---|
Passkeys (WebAuthn)
Build ceremony options for the browser, then verify what credential.toJSON() returns. Store each challenge and accept it once. The passkeys guide shows the whole flow.
- The options builders draw a 32-byte challenge from system entropy, so call them in mutations.
- Verification is native and deterministic. A rejected response throws a failure with code
CRYPTO_ERROR whose message says what failed; a counter that didn’t grow throws WEBAUTHN_COUNTER.
- Attestation statements aren’t evaluated. The options request
"none", which is what passkeys use; other formats are accepted without judging the authenticator model. Cross-origin ceremonies from iframes are rejected.
| API | Contract |
|---|
webauthn.registrationOptions(init): WebAuthnCreationOptions | Options for navigator.credentials.create(): a fresh challenge, a discoverable credential with user verification by default, "none" attestation and your algorithms. Mutations only. |
|---|
webauthn.authenticationOptions(init): WebAuthnRequestOptions | Options for navigator.credentials.get(). Without allow, the browser offers every passkey it holds for the RP. Mutations only. |
|---|
webauthn.verifyRegistration(response, expected): WebAuthnRegistration | Checks the client data (type, challenge, origin), the authenticator data (RP ID hash, user presence, user verification when required, backup flags) and the credential key, then returns the credential to store. |
|---|
webauthn.verifyAuthentication(response, expected): WebAuthnAuthentication | Also checks the credential ID, the signature over the authenticator data and client data hash, the user handle when you pass one, and that the counter grew unless both counters are zero. Store the returned signCount. |
|---|
| API | Contract |
|---|
COSEAlgorithm | -8 EdDSA (Ed25519), -7 ES256, -35 ES384, -36 ES512, -37 PS256, -257 RS256. The default list is EdDSA, ES256, RS256. |
|---|
UserVerification | "required" | "preferred" | "discouraged". The default is "required" everywhere, and it makes verification demand the user-verified flag. Use the same value for options and verification. |
|---|
WebAuthnRegistrationInit | {rp: {id; name}; user: {id; name; displayName?}; exclude?; residentKey?; userVerification?; algorithms?; timeoutMs?}. user.id is an opaque base64url handle of 1 to 64 bytes, never an email address. exclude lists the account’s passkeys as {id; transports?}. residentKey defaults to "required", which is what makes a credential a passkey; timeoutMs to 300000. |
|---|
WebAuthnAuthenticationInit | {rpId; allow?; userVerification?; timeoutMs?}. List allow entries as {id; transports?} to restrict sign-in to one account’s passkeys. |
|---|
WebAuthnCreationOptions | PublicKeyCredentialCreationOptionsJSON. Pass it to PublicKeyCredential.parseCreationOptionsFromJSON(). |
|---|
WebAuthnRequestOptions | PublicKeyCredentialRequestOptionsJSON. Pass it to PublicKeyCredential.parseRequestOptionsFromJSON(). |
|---|
WebAuthnRegistrationExpectation | {challenge; origin: string | readonly string[]; rpId; userVerification?; algorithms?}. Origins match exactly, scheme and port included; Android apps report android:apk-key-hash:… origins. |
|---|
WebAuthnAuthenticationExpectation | The same without algorithms, plus credential, the stored passkey that the response’s id names, and an optional userHandle the response must match. |
|---|
WebAuthnCredential | {id; publicKey; algorithm; signCount; transports; backupEligible; backupState; aaguid}. publicKey is the COSE key in base64url. transports and aaguid are unverified hints. |
|---|
WebAuthnRegistration | {credential; userVerified; attestation: {format}}. |
|---|
WebAuthnAuthentication | {credentialId; signCount; userVerified; backupEligible; backupState; userHandle: string | null}. backupState can change after registration, as a passkey starts or stops syncing. |
|---|
Randomness and limits
- Built-in randomness works only in mutations: not in queries, derived values, or bundle initialization. Elsewhere, pass explicit seeds, keys and nonces.
- Method arguments, stored values and results are JSON. Encode bytes (for example base64) yourself.
- Raw keys stored in bundles or records are ordinary replicated data. Use managed keys to keep private bytes out of your code.
- Input and output sizes are each capped by
FLOWER_RESULT_MAX_BYTES; native memory by FLOWER_RUST_MEMORY_BYTES. Exceeding a budget fails the whole call, even if your code catches the error.
- A long crypto call can't be interrupted midway; the deadline is checked before and after.
- A refused operation, such as verifying an expired or forged token, throws a failure with code
CRYPTO_ERROR. A refusal with a specific cause keeps its code instead: CRYPTO_RANDOM_FORBIDDEN for randomness outside a mutation, or a managed-key code such as KEY_FORBIDDEN. Uncaught, callers receive it as the method’s failure.
See the usage guide, native implementation, and binary guest ABI.