@docstack/abe
v0.1.1
Published
CP-ABE (AC17) primitives for DocStack cryptographic access scopes: rabe compiled to WASM, policy normalization, CEK wrap/unwrap (ADR-0045)
Downloads
199
Maintainers
Readme
@docstack/abe
The cryptography behind DocStack access scopes.
CP-ABE, the AC17 scheme, from rabe compiled to WASM, with the policy normaliser and the authority helpers that mint keys and seal scope content keys. @docstack/client depends on it and loads it lazily, only when a stack declares scopes; application code on a device never imports it.
An access scope is a named set of content whose encrypted fields seal under one 32-byte content key, and that key is sealed under an attribute policy such as ("role:manager" and "dept:sales") or "clearance:secret". A device whose attribute key satisfies the policy opens the scope; one whose key does not holds the same ciphertext and reads null. Denial is decryption failure, not a check, so it binds the device owner too.
Two halves, one module
Authority half: runs where the application controls it, its server, an admin ceremony, a script. Master keys never belong on end-user devices.
import { setup, keygen, wrapCek } from '@docstack/abe';
const { pk, msk } = await setup(); // once; keep msk secret, pk may be public
const aliceKey = await keygen(msk, ['role:hr', 'dept:people']);
const sealed = await wrapCek(pk, '"role:hr" or "clearance:exec"', contentKey); // 32 bytes in, opaque blob outIn practice ClientStack.buildAccessScope({ scopeId, policyString, pk }) from @docstack/client calls wrapCek for you and returns a complete ~AccessScope document: a fresh content key sealed under the policy, its key id, and a per-scope canary. Ship that document in an application patch and every device holds it.
Client half: the one call the engine makes.
import { decryptCek } from '@docstack/abe';
const cek = await decryptCek(attributeKey, sealed); // Uint8Array, or null when the policy is not satisfiedPolicies
import { normalizePolicy, parsePolicy, policyAttributes } from '@docstack/abe';
normalizePolicy('"a" and "b" and "c" and "d"'); // '(("a" and "b") and ("c" and "d"))'
policyAttributes('("role:hr" or "clearance:exec")'); // ['role:hr', 'clearance:exec']Formulas are monotone: and, or, parentheses, "kind:value" literals. No negation, no code. The scheme's converter needs balanced parentheses and panics on long unbalanced and chains, so every sealing call normalises the formula first; normalizePolicy throws on not.
Keys and blobs are opaque
Every key and ciphertext is a string. rabe serialises 64-bit limbs that do not survive a JavaScript JSON round-trip, so store and transport these blobs verbatim and never parse them.
Status
Experimental cryptography, unaudited. rabe's BN254 backend carries roughly a 100-bit modern security margin; an audit gates any production claim. The reasoning, the alternatives measured and the residual risks are in ADR-0045 and the threat model.
Building from source
The WASM under src/wasm is vendored and regenerated by npm run build:wasm, which needs the Rust toolchain with the wasm32-unknown-unknown target and wasm-bindgen-cli matching the version in rust/Cargo.lock. npm run build compiles the TypeScript and copies the vendored artifacts into lib/; npm test runs a smoke test through the WASM.
Documentation
- Scope your data, the guide
- Access control, the model, its formula language and its limits
- API reference
License
CC-BY-SA-4.0 · © Onyx AC, LLC. rabe is licensed under the MIT license.
