Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

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.

Last updated on August 4, 2026

Was this page helpful?