跳到主要内容

rxdb-adapter-encrypted

Local field-level AES-GCM-256 envelope encryption for @aiao/rxdb. Plugs into the SQLite-core / PGlite / wa-sqlite / sqliteai adapters with zero-plaintext-at-rest guarantees on the structural database files, change log, query cache and history snapshots.


Install​

pnpm add @aiao/rxdb-adapter-encrypted

Peer of any local SQLite-family adapter (@aiao/rxdb-adapter-wa-sqlite, @aiao/rxdb-adapter-pglite, @aiao/rxdb-adapter-sqlite-wasm, @aiao/rxdb-adapter-sqliteai). Not needed for Supabase or remote-only adapters.


Quickstart​

import { Entity, EntityBase, PropertyType, RxDB, SyncType } from '@aiao/rxdb';
import { RxDBAdapterWaSqlite } from '@aiao/rxdb-adapter-wa-sqlite';

@Entity({
name: 'User',
tableName: 'users',
properties: [
{ name: 'displayName', type: PropertyType.string },
`{ name: 'email', type: PropertyType.string, encrypted: true }`
]
})
class User extends EntityBase {
displayName!: string;
email!: string;
}

const rxdb = new RxDB({
dbName: 'app.db',
context: { userId: 'current-user' },
entities: [User],
sync: `{ local: { adapter: 'wa-sqlite' }, type: SyncType.None }`
});

rxdb.adapter('wa-sqlite', async db => new RxDBAdapterWaSqlite(db, { vfs: 'OPFSAdaptiveVFS' }));
await rxdb.connect('wa-sqlite');

const adapter = await rxdb.getAdapter('wa-sqlite');

await adapter.encryption.unlock({
passphrase: 'correct horse battery staple'
// idleTimeoutMs: 10 * 60_000 // override default 5-minute auto-lock
// idleTimeoutMs: 0 // disable auto-lock
});

await rxdb.entityManager.getRepository(User).create(
new User({
displayName: 'Ada',
email: 'ada@example.com' // encrypted at rest
})
);

Public API​

import {
// Keyring lifecycle
Keyring,
createKeyring,
type UnlockOptions,
type PassphraseUnlockOptions,
type KeyBytesUnlockOptions,
type CryptoKeyUnlockOptions,
type KeyProviderUnlockOptions,
type LegacyEnvelopePolicy,

// Envelope codec
encodeEnvelope,
decodeEnvelope,
isEnvelope,
buildAAD,
ENVELOPE_VERSION,
ENVELOPE_ALG,
type CryptoEnvelope,
type EnvelopeVersion,

// Schema + query validators
validateEncryptedPropertyMetadata,
validateFTSRegistrationAgainstEncryptedColumns,
validateQueryAgainstEncryptedColumns,
type EncryptedAwareEntity,

// Typed errors
EncryptedError,
EncryptedConfigurationError,
EncryptedDecryptError,
EncryptedLockedError,
EncryptedQueryError,
EncryptedUnlockError,
type EncryptedErrorCode,
type EncryptedErrorInit,

// Keyring persistence binding
type KeyringRow,
type KeyringStorageBinding,

// Verifier sentinel constant
VERIFIER_SENTINEL
} from '@aiao/rxdb-adapter-encrypted';

import { scanForPlaintext, type ScanHit } from '@aiao/rxdb-adapter-encrypted/testing';

Refer to the TSDoc on each export for its security and lifecycle contract.


Guarantees​

SpecGuarantee
FR-001Schema validation rejects encrypted PK / FK / index / unique / sortable / FTS / computed columns
FR-002Envelope text form v|alg|kid|iv|ct|tag; new writes use v2
FR-003AES-GCM-256 with a unique 96-bit IV; length-prefixed AAD binds the database, entity namespace, table, column, typed ID and kid
FR-004unlock() accepts exactly one of passphrase / keyBytes / key / keyProvider
FR-005All encrypted columns are emitted as TEXT regardless of logical type
FR-006Zero plaintext in DB files, rxdb_change patches, query cache, history snapshots
US-804Encrypted bigint is signed 64-bit decimal; binary copies the current Uint8Array view and restores an independent Uint8Array
FR-007Filter / order / group / projection over encrypted columns throws EncryptedQueryError; FTS registration throws EncryptedConfigurationError('encrypted_fts_forbidden')
FR-008Locked-state reads throw EncryptedLockedError; idle auto-lock after 5 min (configurable, 0 disables)
FR-009unlock() verifies passphrase against persisted verifier probe; wrong passphrase never retains key

Migrating v1 Envelopes​

v1 entity envelopes are rejected by default because their AAD does not bind the entity namespace and uses ambiguous delimiter boundaries. Enable legacy reads only for a bounded migration:

await adapter.encryption.unlock({
passphrase: 'correct horse battery staple',
legacyEnvelopePolicy: 'migration'
});

Read and rewrite every encrypted entity while this policy is active. Every write produces v2. After the rewrite, lock and unlock without legacyEnvelopePolicy; any remaining v1 entity envelope then fails with EncryptedDecryptError.code === 'legacy_envelope_forbidden'. Existing v1 keyring verifiers remain readable during unlock so a legacy database can enter the migration flow. A failed v2 authentication is never retried with v1 AAD.


What this package does NOT do (MVP)​

  • No full-database encryption — only declared columns are sealed.
  • No native keychain / passkey / WebAuthn — bring your own passphrase or key bytes.
  • No searchable encryption — where / order / group / FTS on encrypted columns is rejected.
  • No key rotation — single kid per database for the MVP.
  • No audit log — decrypt failures throw, not persisted.
  • No automatic relock on tab visibility — subscribe to document.visibilitychange yourself.

License​

MIT

Classes​

ClassDescription
EncryptedConfigurationErrorSchema / 初始化 / 生命周期阶段的配置错误。
EncryptedDecryptError读取时信封层失败。
EncryptedError@aiao/rxdb-adapter-encrypted 抛出的所有错误的抽象基类。
EncryptedLockedError在 Keyring.isLocked 时进入 encrypt / decrypt 路径触发。
EncryptedQueryError查询引用了加密列。
EncryptedUnlockErrorunlock() 密钥校验或 provider 分发失败,或被 lock() 取消。
Keyring数据库级 AES-GCM-256 密钥生命周期与单元格信封加解密器。

Interfaces​

InterfaceDescription
CryptoEnvelope解码后的 AES-GCM 信封字段。二进制字段均为独立的 Uint8Array。
CryptoKeyUnlockOptions通过具备 encrypt/decrypt usage 的 AES-GCM-256 CryptoKey 解锁。
DecryptArgs单元格解密参数。实体 namespace、表、列和类型化主键必须与加密时的 AAD 身份一致。
EncryptArgs单元格加密参数。除明文字节外,其余字段共同组成 v2 AAD 身份,解密时必须完全一致。
EncryptedAwareEntityschema 与查询安全校验所需的最小实体元数据形状。
EncryptedErrorInit所有加密错误共享的结构化上下文。字段均可安全用于错误分诊。
KeyBytesUnlockOptions通过恰好 32 字节的原始 AES-GCM-256 密钥解锁。
KeyProviderUnlockOptions通过异步 provider 获取 CryptoKey 或 32 字节原始密钥。provider 失败会被结构化包装。
KeyringRow持久化在 rxdb_db_keyring 表中的那一行。
KeyringStorageBinding由各 adapter(sqlite-core、pglite …)实现,用于读写 keyring 单例行。
PassphraseUnlockOptions通过 PBKDF2 passphrase 派生并校验持久化 keyring 的解锁参数。
PatchWalkArgsundo/redo change patch 加解密所需的实体身份、主键和 keyring。

Type Aliases​

Type AliasDescription
EncryptedEntityResolver按实体名与可选 namespace 解析关系目标元数据。返回 undefined 会使跨实体查询 fail-closed。
EncryptedErrorCode所有具体子类 code 字面量的联合类型。
EnvelopeVersion可被解析的信封版本;新写入固定使用 v2,v1 仅供显式迁移读取。
LegacyEnvelopePolicyv1 实体信封读取策略;所有新写入始终生成 v2。
UnlockOptions四种互斥密钥来源之一,可附带空闲锁定与旧信封迁移策略。

Variables​

VariableDescription
ENVELOPE_ALG冻结的当前算法标签。
ENVELOPE_VERSION冻结的当前信封版本。
VERIFIER_SENTINELkeyring verifier 探测加密用的固定明文。

Functions​

FunctionDescription
buildAAD-
createKeyring创建初始锁定、绑定指定数据库认证域与持久化存储的 Keyring。
decodeEnvelope-
deserializeFromEnvelope按持久化时的属性类型把已认证明文字节还原为业务值。
encodeEnvelope-
envelopePlaintextPatches遍历明文补丁的顶层键;对于 entity.encryptedPropertyMap 中存在的键, 将值替换为 keyring.encrypt 生成的信封字符串。未加密的键直接复制。
isEnvelope-
serializeForEnvelope按属性类型把单个非空业务值编码为信封明文字节。
unenvelopePlaintextPatchesenvelopePlaintextPatches 的逆操作:将顶层信封字符串解密回明文。 应用于 undo/redo 的 inversePatch 时使用。
validateEncryptedPropertyMetadata校验加密属性不能参与主键、外键、索引、唯一、排序、计算列或 FTS。
validateEncryptedQuery把 RxDB 查询参数映射到通用加密查询校验器。
validateFTSRegistrationAgainstEncryptedColumns拒绝把加密列注册进 FTS 索引。
validateQueryAgainstEncryptedColumns拒绝在加密列上过滤、排序、分组或显式投影。