Crypto engine reference
Reference for the production sodium engine, opaque key handles, algorithms, persisted formats, worker isolation, and deterministic test helpers.
@ownfold/crypto supplies the production CryptoEngine. It performs cryptographic operations but
does not decide user identity, store vault state, make network requests, or render UI.
pnpm add @ownfold/core@beta @ownfold/crypto@beta
createSodiumCryptoEngine()
import { createSodiumCryptoEngine } from "@ownfold/crypto"
const cryptoEngine = createSodiumCryptoEngine()
const rootKey = await cryptoEngine.createRootKey()
if (rootKey.status === "error") return rootKey
The factory is synchronous; the returned engine initializes its crypto runtime lazily when an
operation first needs it. Every fallible operation returns Result.
createRootKey() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>
() => Promise<Result$1<RootKeyHandle, CryptoEngineError>>encryptRecord(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>
(input: EncryptRecordInput) => Promise<Result$1<EncryptedEnvelopeValue, CryptoError>>decryptRecord(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>
(input: DecryptRecordInput) => Promise<Result$1<Uint8Array<ArrayBufferLike>, CryptoError>>rewrapRecordDataKey(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>
(input: RewrapRecordDataKeyInput) => Promise<Result$1<EncryptedEnvelopeV2, CryptoError>>createRecoveryKit(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>
(input: CreateRecoveryKitInput) => Promise<Result$1<RecoveryKitV1, CryptoError>>openRecoveryKit(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
(input: OpenRecoveryKitInput) => Promise<Result$1<RootKeyHandle, CryptoError>>createDeviceKey() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>
() => Promise<Result$1<DeviceKeyHandle, CryptoEngineError>>wrapRootKeyForDevice(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>
(input: WrapRootKeyForDeviceInput) => Promise<Result$1<DeviceEnvelopeV1, CryptoError>>openDeviceEnvelope(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>
(input: OpenDeviceEnvelopeInput) => Promise<Result$1<RootKeyHandle, CryptoError>>compareRootKeys(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>
(left: RootKeyHandle, right: RootKeyHandle) => Promise<Result$1<boolean, CryptoEngineError>>destroyRootKey(key: RootKeyHandle) => void
(key: RootKeyHandle) => voidKey handles
RootKeyHandle and DeviceKeyHandle deliberately expose identity metadata, not private key bytes.
The engine is the only component allowed to dereference their private material.
const created = await cryptoEngine.createRootKey()
if (created.status === "error") return created
try {
// Pass the handle back to engine operations.
} finally {
cryptoEngine.destroyRootKey(created.value)
}
After destroyRootKey, the handle reports destroyed: true and must not produce a successful
operation. Destroying a handle reduces key lifetime; it does not erase ciphertext, Recovery Kits, or
device envelopes.
Record encryption
rootKeyRootKeyHandle
RootKeyHandlevaultIdstring
stringkeyVersionnumber
numbercontextRecordContext
RecordContextencoding"json" | "binary"
"json" | "binary"plaintextUint8Array
Uint8ArrayenvelopeVersion?1 | 2
1 | 2encryptRecord:
- generates a fresh record data key;
- encrypts plaintext with XChaCha20-Poly1305;
- authenticates vault ID, key version, encoding, namespace, record ID, and owner ID;
- wraps the data key under the root key; and
- returns a versioned envelope.
const encrypted = await cryptoEngine.encryptRecord({
rootKey,
vaultId: "vault_01",
keyVersion: 1,
context: {
namespace: "documents",
recordId: "doc_01",
ownerId: "user_01",
},
encoding: "binary",
plaintext: bytes,
})
decryptRecord requires the exact expected context and returns fresh Uint8Array plaintext only
after format, context, and AEAD authentication succeed.
Data-key rewrapping
oldRootKeyRootKeyHandle
RootKeyHandlenewRootKeyRootKeyHandle
RootKeyHandleenvelopeEncryptedEnvelopeValue
EncryptedEnvelopeValuenewKeyVersionnumber
numberrewrapRecordDataKey opens only the wrapped record data key under the old root key and wraps it
under the new root key. It returns a V2 envelope with the existing ciphertext and authenticated
context. Application plaintext is never decrypted during rotation.
Recovery Kits
rootKeyRootKeyHandle
RootKeyHandlevaultIdstring
stringkeyVersionnumber
numbercreatedAtstring
stringsecretUint8Array
Uint8ArraycreateRecoveryKit derives a wrapping key from the supplied secret using the parameters embedded in
the kit and encrypts the root key locally. openRecoveryKit validates the structure, derives the
same key, and returns an opaque root-key handle only after authentication succeeds.
Recovery failures intentionally do not reveal whether the secret, ciphertext, or authenticated metadata was wrong. Never upload the secret or file to diagnose them.
Device keys and envelopes
| Method | Purpose |
|---|---|
createDeviceKey() |
Generates a local X25519 identity and returns an opaque private-key handle plus public key. |
wrapRootKeyForDevice(input) |
Creates a recipient-bound, versioned device envelope. |
openDeviceEnvelope(input) |
Opens an envelope only with the matching device private key. |
compareRootKeys(left, right) |
Constant-behavior equality check used by recovery verification. |
rootKeyRootKeyHandle
RootKeyHandledeviceIdstring
stringvaultIdstring
stringkeyVersionnumber
numbercreatedAtstring
stringrecipientPublicKeystring
stringDevice public keys and encrypted envelopes may cross the network. Device private keys and opened root handles must remain in local protected storage or memory.
Algorithms and format versions
| Artifact | Current suite | Read versions | Write version |
|---|---|---|---|
| Record envelope | XCHACHA20-POLY1305-IETF |
1, 2 | 2 |
| Recovery Kit | ARGON2ID13(3,67108864,32)+XCHACHA20-POLY1305-IETF |
1 | 1 |
| Device envelope | X25519-HKDF-SHA-256+A256GCM |
1 | 1 |
| Protected device key | X25519+A256GCM |
1 | 1 |
| Pairing offer | Structured public metadata | 1 | 1 |
The persisted value carries its own format, version, algorithm identifiers, nonce, salt, and authenticated context. Do not strip or replace those fields in database mappings.
Browser worker entry point
The worker entry point belongs to @ownfold/browser/worker, but it exposes the same production
engine through a message boundary.
import { exposeOwnfoldCryptoWorker } from "@ownfold/browser/worker"
exposeOwnfoldCryptoWorker()
import { createVaultClient, createWorkerCryptoEngine } from "@ownfold/browser"
const worker = new Worker(
new URL("../ownfold-crypto.worker.ts", import.meta.url),
{ type: "module" },
)
export const vault = createVaultClient({
cryptoEngine: createWorkerCryptoEngine({ worker }),
transport,
})
Worker isolation reduces accidental main-thread key exposure. Same-origin script compromise can still call an unlocked worker through the application, so keep CSP, dependency integrity, and automatic locking controls.
Testing entry point
Import deterministic helpers only from @ownfold/crypto/testing.
| Export | Behavior |
|---|---|
createDeterministicCryptoEngine(values) |
Consumes caller-provided byte arrays as deterministic randomness. |
withEnvelopeVersionForTesting(engine, version) |
Forces record writes to envelope V1 or V2 for compatibility tests. |
import {
createDeterministicCryptoEngine,
withEnvelopeVersionForTesting,
} from "@ownfold/crypto/testing"
Never ship the testing entry point in production. Exhausted or incorrectly sized deterministic input produces a typed crypto failure; it never falls back to insecure randomness.
Error surface
Operations return CryptoError, which includes invalid input, corrupted/authentication failures,
unavailable key versions, Recovery Kit failures, and runtime failures. Handle them as values and
destroy transient handles in cleanup paths. See Errors.