Trusted backend encryption
Encrypt and decrypt records inside Node.js or another server runtime with Ownfold's low-level cryptographic engine.
@ownfold/crypto can run without @ownfold/browser, React, an HTTP handler, or a database adapter.
Use it in a service, worker, CLI, or scheduled job when the backend intentionally owns the
encryption boundary.
Install
pnpm add @ownfold/core@beta @ownfold/crypto@beta
Create and use a root key
import { Utf8 } from "@ownfold/core"
import { createSodiumCryptoEngine } from "@ownfold/crypto"
const engine = createSodiumCryptoEngine()
const created = await engine.createRootKey()
if (created.status === "error") {
throw new Error(`Root-key creation failed: ${created.error.code}`)
}
const rootKey = created.value
try {
const encrypted = await engine.encryptRecord({
rootKey,
vaultId: "service-vault",
keyVersion: 1,
context: {
namespace: "invoices",
recordId: "invoice-42",
ownerId: "tenant-7",
},
encoding: "json",
plaintext: Utf8.encode(JSON.stringify({ total: 4200 })),
})
if (encrypted.status === "error") {
throw new Error(`Record encryption failed: ${encrypted.error.code}`)
}
await records.insert(encrypted.value)
} finally {
engine.destroyRootKey(rootKey)
}
Root-key handles are intentionally opaque and are not serializable. Destroy them as soon as the operation completes.
Decrypt with authenticated context
const decrypted = await engine.decryptRecord({
rootKey,
envelope,
expectedContext: {
namespace: "invoices",
recordId: "invoice-42",
ownerId: "tenant-7",
},
})
if (decrypted.status === "error") {
return { ok: false, code: decrypted.error.code }
}
const decoded = Utf8.decode(decrypted.value)
if (decoded.status === "error") return { ok: false, code: decoded.error.code }
return { ok: true, json: decoded.value }
Namespace, record ID, and optional owner ID are authenticated. Decryption fails when callers use a different context or when stored envelope fields were modified.
Persist access safely
Do not serialize a root-key handle or write raw root-key bytes. Create a Recovery Kit protected by a secret from a dedicated secret manager, store the versioned kit separately, and open it only for the duration of a job:
const opened = await engine.openRecoveryKit({
kit: persistedRecoveryKit,
secret: secretManagerValue,
})
if (opened.status === "error") {
return { ok: false, code: opened.error.code }
}
try {
await runEncryptionJob(opened.value)
} finally {
engine.destroyRootKey(opened.value)
}
Keep the Recovery Kit and its secret in separate systems. Rotate access to the secret manager, restrict decryption workers by role, and never log the secret, plaintext, or serialized kit.
Binary records
Set encoding: "binary" and pass the original Uint8Array. Ownfold does not base64-encode your
plaintext before encryption. The resulting envelope is JSON-safe and can be stored in a JSON or
text column after validation.
Error handling
All expected cryptographic failures are typed result values. Important cases include:
| Error | Meaning | Recovery |
|---|---|---|
InvalidInputError |
Context, bytes, or version input is invalid. | Reject the operation and correct the caller. |
AuthenticationFailedError |
Ciphertext or authenticated context does not verify. | Preserve the stored value for investigation; do not retry as success. |
KeyVersionUnavailableError |
The supplied root key cannot open this record version. | Load the correct key version or complete rotation recovery. |
RecoveryAuthenticationFailedError |
The Recovery Kit secret or kit authentication is invalid. | Verify secret lookup and kit identity; do not reveal which input was wrong. |
When to use browser E2EE instead
Use @ownfold/browser when users—not the backend—must control plaintext and recovery. In that mode,
the backend uses @ownfold/server only for ciphertext coordination. See the
frontend SDK overview and threat model.