@sudp-protocol/authorizer 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,10 +16,12 @@ shapes, and the wrap-key derivation.
16
16
 
17
17
  ```
18
18
  @sudp-protocol/authorizer ← carrier-agnostic protocol primitives
19
- canonicalize, sha256, computeBinding,
19
+ canonicalize, sha256,
20
+ computeBinding, computeBatchBinding,
20
21
  deriveWrappingKey, wrapBindingAd, sealAd,
21
- aeadSeal, aeadOpen, base64url helpers,
22
- DS_BIND / DS_WRAP / DS_SEAL constants
22
+ aeadSeal, aeadOpen, aeadEncrypt, base64url helpers,
23
+ sealRecord, unsealRecord, recordAad, deriveItemKey, ← per-record (per-item) seal
24
+ DS_BIND / DS_WRAP / DS_SEAL / DS_ITEM constants
23
25
 
24
26
  @sudp-protocol/authorizer/webauthn ← WebAuthn-specific adapter
25
27
  prfToUserKey(prfOutput) → 32-byte y_c
@@ -69,6 +71,16 @@ const wrapped = aeadSeal(Wc, plaintext, wrapBindingAd(credentialId));
69
71
  // part of the grant.
70
72
  ```
71
73
 
74
+ For batch grants, swap `computeBinding` for `computeBatchBinding(DS_BIND, r, ops)` — same math, one signature covers `ops = (o_1, …, o_n)`.
75
+
76
+ For **per-item vaults** (a vault stored as many independently-encrypted records
77
+ instead of one blob), seal/open a single record with `sealRecord(k, ctx, pt)` /
78
+ `unsealRecord(k, ctx, sealed)`, where `ctx: SealCtx` is `{ domain, vault, id,
79
+ version }`. sudp binds `ctx` into the AEAD AAD (anti cross-vault / cross-id /
80
+ version-mismatch); the record body, id derivation, and conflict resolution are
81
+ the caller's. Byte-identical to the Rust crate's `seal_record` — see the
82
+ [conformance map](../../README.md#cross-language-alignment).
83
+
72
84
  ## End-to-end protocol walkthrough
73
85
 
74
86
  For how this package fits with the Rust Custodian and the Requester
package/dist/aad.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare const WRAP_VERSION = 1;
11
11
  * DS_WRAP ‖ credentialId ‖ ver_be(u16, big-endian)
12
12
  *
13
13
  * Bound as associated data when sealing/opening `K̂_c` under `W_c`, so a
14
- * peer-wrapped record cannot be substituted across credentials or versions.
14
+ * per-credential-wrapped record cannot be substituted across credentials or versions.
15
15
  */
16
16
  export declare function wrapBindingAd(credentialId: Uint8Array, wrapVersion?: number): Uint8Array;
17
17
  /**
package/dist/aad.js CHANGED
@@ -12,7 +12,7 @@ export const WRAP_VERSION = 0x0001;
12
12
  * DS_WRAP ‖ credentialId ‖ ver_be(u16, big-endian)
13
13
  *
14
14
  * Bound as associated data when sealing/opening `K̂_c` under `W_c`, so a
15
- * peer-wrapped record cannot be substituted across credentials or versions.
15
+ * per-credential-wrapped record cannot be substituted across credentials or versions.
16
16
  */
17
17
  export function wrapBindingAd(credentialId, wrapVersion = WRAP_VERSION) {
18
18
  return concatBytes(DS_WRAP, credentialId, u16beBytes(wrapVersion));
package/dist/binding.d.ts CHANGED
@@ -19,4 +19,18 @@ export declare const DS_BIND: Uint8Array<ArrayBufferLike>;
19
19
  * deployment.
20
20
  */
21
21
  export declare function computeBinding(domain: Uint8Array, r: Uint8Array, op: unknown): Promise<Uint8Array>;
22
+ /**
23
+ * Batch counterpart of {@link computeBinding}:
24
+ *
25
+ * β = SHA-256(domain ‖ r ‖ SHA-256(canonical(ops)))
26
+ *
27
+ * where `ops` is a JSON array of operations. Byte-aligned with the Rust
28
+ * crate's `compute_beta_from_canonical(domain, r, &BatchOperations(ops).canonical_bytes())`.
29
+ *
30
+ * Semantically identical to `computeBinding(domain, r, ops)` because the
31
+ * canonical encoder treats arrays uniformly, but named separately so the
32
+ * "batch" intent is explicit at the call site (and so a single conformance
33
+ * vector pins the batch shape independently).
34
+ */
35
+ export declare function computeBatchBinding(domain: Uint8Array, r: Uint8Array, ops: readonly unknown[]): Promise<Uint8Array>;
22
36
  //# sourceMappingURL=binding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,OAAO,6BAAuB,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,CAAC,EAAE,UAAU,EACb,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,UAAU,CAAC,CAGrB"}
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,OAAO,6BAAuB,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,CAAC,EAAE,UAAU,EACb,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,CAAC,EAAE,UAAU,EACb,GAAG,EAAE,SAAS,OAAO,EAAE,GACtB,OAAO,CAAC,UAAU,CAAC,CAErB"}
package/dist/binding.js CHANGED
@@ -25,4 +25,20 @@ export async function computeBinding(domain, r, op) {
25
25
  const opHash = await sha256(canonicalize(op));
26
26
  return sha256(concatBytes(domain, r, opHash));
27
27
  }
28
+ /**
29
+ * Batch counterpart of {@link computeBinding}:
30
+ *
31
+ * β = SHA-256(domain ‖ r ‖ SHA-256(canonical(ops)))
32
+ *
33
+ * where `ops` is a JSON array of operations. Byte-aligned with the Rust
34
+ * crate's `compute_beta_from_canonical(domain, r, &BatchOperations(ops).canonical_bytes())`.
35
+ *
36
+ * Semantically identical to `computeBinding(domain, r, ops)` because the
37
+ * canonical encoder treats arrays uniformly, but named separately so the
38
+ * "batch" intent is explicit at the call site (and so a single conformance
39
+ * vector pins the batch shape independently).
40
+ */
41
+ export async function computeBatchBinding(domain, r, ops) {
42
+ return computeBinding(domain, r, ops);
43
+ }
28
44
  //# sourceMappingURL=binding.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAkB,EAClB,CAAa,EACb,EAAW;IAEX,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAkB,EAClB,CAAa,EACb,EAAW;IAEX,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAkB,EAClB,CAAa,EACb,GAAuB;IAEvB,OAAO,cAAc,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;AACxC,CAAC"}
package/dist/bytes.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  export declare function utf8(s: string): Uint8Array;
2
2
  export declare function concatBytes(...parts: readonly Uint8Array[]): Uint8Array;
3
3
  export declare function u16beBytes(n: number): Uint8Array;
4
+ export declare function u32beBytes(n: number): Uint8Array;
5
+ /**
6
+ * 8-byte big-endian encoding of a `u64`. Takes a `bigint` so the full unsigned
7
+ * 64-bit range is exact (a `number` is only safe to 2^53). MUST match the Rust
8
+ * crate's `u64::to_be_bytes`.
9
+ */
10
+ export declare function u64beBytes(n: bigint): Uint8Array;
4
11
  export declare function bytesToB64Url(b: Uint8Array): string;
5
12
  export declare function b64UrlToBytes(s: string): Uint8Array;
6
13
  //# sourceMappingURL=bytes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAEA,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAE1C;AAED,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAUvE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAKhD;AAcD,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAgBnD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAiCnD"}
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAEA,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAE1C;AAED,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAUvE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAKhD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAKhD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAWhD;AAcD,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAgBnD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAiCnD"}
package/dist/bytes.js CHANGED
@@ -20,6 +20,29 @@ export function u16beBytes(n) {
20
20
  }
21
21
  return new Uint8Array([(n >> 8) & 0xff, n & 0xff]);
22
22
  }
23
+ export function u32beBytes(n) {
24
+ if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) {
25
+ throw new Error(`u32beBytes: out of range: ${n}`);
26
+ }
27
+ return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
28
+ }
29
+ /**
30
+ * 8-byte big-endian encoding of a `u64`. Takes a `bigint` so the full unsigned
31
+ * 64-bit range is exact (a `number` is only safe to 2^53). MUST match the Rust
32
+ * crate's `u64::to_be_bytes`.
33
+ */
34
+ export function u64beBytes(n) {
35
+ if (n < 0n || n > 0xffffffffffffffffn) {
36
+ throw new Error(`u64beBytes: out of range: ${n}`);
37
+ }
38
+ const out = new Uint8Array(8);
39
+ let v = n;
40
+ for (let i = 7; i >= 0; i--) {
41
+ out[i] = Number(v & 0xffn);
42
+ v >>= 8n;
43
+ }
44
+ return out;
45
+ }
23
46
  const B64URL_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
24
47
  const B64URL_LOOKUP = (() => {
25
48
  const t = new Int8Array(256).fill(-1);
package/dist/bytes.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -10,11 +10,12 @@
10
10
  * For the WebAuthn PRF → y_c adapter and assertion helpers, import from
11
11
  * `@sudp-protocol/authorizer/webauthn`.
12
12
  */
13
- export { utf8, concatBytes, u16beBytes, bytesToB64Url, b64UrlToBytes, } from "./bytes.js";
13
+ export { utf8, concatBytes, u16beBytes, u32beBytes, u64beBytes, bytesToB64Url, b64UrlToBytes, } from "./bytes.js";
14
14
  export { canonicalize } from "./canonical.js";
15
15
  export { sha256 } from "./hash.js";
16
- export { computeBinding, DS_BIND } from "./binding.js";
16
+ export { computeBatchBinding, computeBinding, DS_BIND } from "./binding.js";
17
17
  export { deriveWrappingKey } from "./kdf.js";
18
18
  export { wrapBindingAd, sealAd, DS_WRAP, DS_SEAL, WRAP_VERSION } from "./aad.js";
19
19
  export { aeadEncrypt, aeadSeal, aeadOpen } from "./aead.js";
20
+ export { recordAad, deriveItemKey, sealRecord, unsealRecord, DS_ITEM, RECORD_SUITE_XCHACHA20POLY1305, type SealCtx, } from "./record.js";
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,IAAI,EACJ,WAAW,EACX,UAAU,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,SAAS,EACT,aAAa,EACb,UAAU,EACV,YAAY,EACZ,OAAO,EACP,8BAA8B,EAC9B,KAAK,OAAO,GACb,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -10,11 +10,12 @@
10
10
  * For the WebAuthn PRF → y_c adapter and assertion helpers, import from
11
11
  * `@sudp-protocol/authorizer/webauthn`.
12
12
  */
13
- export { utf8, concatBytes, u16beBytes, bytesToB64Url, b64UrlToBytes, } from "./bytes.js";
13
+ export { utf8, concatBytes, u16beBytes, u32beBytes, u64beBytes, bytesToB64Url, b64UrlToBytes, } from "./bytes.js";
14
14
  export { canonicalize } from "./canonical.js";
15
15
  export { sha256 } from "./hash.js";
16
- export { computeBinding, DS_BIND } from "./binding.js";
16
+ export { computeBatchBinding, computeBinding, DS_BIND } from "./binding.js";
17
17
  export { deriveWrappingKey } from "./kdf.js";
18
18
  export { wrapBindingAd, sealAd, DS_WRAP, DS_SEAL, WRAP_VERSION } from "./aad.js";
19
19
  export { aeadEncrypt, aeadSeal, aeadOpen } from "./aead.js";
20
+ export { recordAad, deriveItemKey, sealRecord, unsealRecord, DS_ITEM, RECORD_SUITE_XCHACHA20POLY1305, } from "./record.js";
20
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,IAAI,EACJ,WAAW,EACX,UAAU,EACV,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EACL,SAAS,EACT,aAAa,EACb,UAAU,EACV,YAAY,EACZ,OAAO,EACP,8BAA8B,GAE/B,MAAM,aAAa,CAAC"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Per-record seal/unseal — the per-item vault primitive, authorizer-side mirror
3
+ * of the Rust crate's `sudp::state::{seal_record, unseal_record}`.
4
+ *
5
+ * Eats bytes, spits bytes: the record's business structure, serialization,
6
+ * id derivation, version comparison, merge, tombstones, and GC are ALL the
7
+ * caller's concern. This module only seals one record under the vault key `K`
8
+ * bound — via AEAD associated data — to a structured {@link SealCtx}, so a
9
+ * record can never be opened under a different vault / id / version / domain.
10
+ *
11
+ * The AAD binding detects cross-vault splicing, cross-id substitution,
12
+ * version/ciphertext mismatch, and cross-domain confusion. It does NOT by
13
+ * itself prevent rollback (an attacker substituting a strictly older but
14
+ * validly-sealed record with its matching older `version`) — keep a monotonic
15
+ * per-id version store on the caller side, and put richer causal/merge metadata
16
+ * (HLC, device id, …) INSIDE the plaintext so the AEAD authenticates it too.
17
+ *
18
+ * Every byte layout here (canonical AAD, HKDF info, sealed framing) MUST stay
19
+ * byte-for-byte aligned with `custodian/rust/src/state/record.rs`; the
20
+ * conformance vectors in `test/conformance.test.ts` are pinned against the Rust
21
+ * `assert_eq!` anchors.
22
+ */
23
+ /** Per-record (per-item) domain-separation label. */
24
+ export declare const DS_ITEM: Uint8Array<ArrayBufferLike>;
25
+ /** Suite/format tag: `0x01` = XChaCha20-Poly1305 + canonical AAD v1. */
26
+ export declare const RECORD_SUITE_XCHACHA20POLY1305 = 1;
27
+ /**
28
+ * Structured sealing context for one record. sudp builds the AEAD associated
29
+ * data from these fields; callers MUST NOT hand-assemble opaque AAD.
30
+ */
31
+ export interface SealCtx {
32
+ /** Purpose separation within the per-record domain, e.g. `"item"` / `"keyset"`. */
33
+ domain: string;
34
+ /** Vault identifier (anti cross-vault splice). */
35
+ vault: Uint8Array;
36
+ /** Opaque record id — typically `HMAC_K(name)`; never interpreted (anti X-as-Y). */
37
+ id: Uint8Array;
38
+ /**
39
+ * Opaque, monotonic-per-id ordering value. sudp binds it into the AAD but
40
+ * never interprets it — meaning (server sequence number, Lamport/HLC stamp,
41
+ * version-vector, …) and the whole conflict-resolution strategy are the
42
+ * caller's. Recommended scalar encoding for the common case: `u64`
43
+ * big-endian (`u64beBytes(n)`). Binds the ciphertext to a version (detects
44
+ * mismatch, not rollback).
45
+ */
46
+ version: Uint8Array;
47
+ }
48
+ /**
49
+ * Canonical, length-prefixed associated data for a per-record seal:
50
+ *
51
+ * AAD = suite(1) ‖ lp(DS_ITEM) ‖ lp(domain_utf8) ‖ lp(vault) ‖ lp(id) ‖ lp(version)
52
+ * where lp(x) = len_be(x.length as u32, 4 bytes) ‖ x
53
+ *
54
+ * MUST be byte-identical to the Rust crate's `record_aad`.
55
+ */
56
+ export declare function recordAad(suite: number, ctx: SealCtx): Uint8Array;
57
+ /**
58
+ * Derive the per-record AEAD key `K_aead` from the vault key `k`:
59
+ *
60
+ * K_aead = HKDF-SHA-256(ikm = k, salt = "", info = DS_ITEM)
61
+ *
62
+ * Domain-separated so `K_aead` never shares raw bytes with the caller's
63
+ * `HMAC_K(name)` record-id derivation. MUST match the Rust crate's
64
+ * `derive_item_key`.
65
+ */
66
+ export declare function deriveItemKey(k: Uint8Array): Promise<Uint8Array>;
67
+ /**
68
+ * Seal one record. Output layout: `suite(1) ‖ nonce(24) ‖ ciphertext ‖ tag(16)`.
69
+ * A fresh random 24-byte nonce is generated per call (Web Crypto).
70
+ */
71
+ export declare function sealRecord(k: Uint8Array, ctx: SealCtx, plaintext: Uint8Array): Promise<Uint8Array>;
72
+ /**
73
+ * Unseal one record. Throws on any authentication failure (AAD mismatch and
74
+ * tag mismatch are indistinguishable — the same Poly1305 check) or on a
75
+ * structurally malformed blob (empty / unknown suite tag).
76
+ */
77
+ export declare function unsealRecord(k: Uint8Array, ctx: SealCtx, sealed: Uint8Array): Promise<Uint8Array>;
78
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,qDAAqD;AACrD,eAAO,MAAM,OAAO,6BAAuB,CAAC;AAE5C,wEAAwE;AACxE,eAAO,MAAM,8BAA8B,IAAO,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,KAAK,EAAE,UAAU,CAAC;IAClB,oFAAoF;IACpF,EAAE,EAAE,UAAU,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,UAAU,CAAC;CACrB;AAOD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,UAAU,CASjE;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBtE;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,CAAC,EAAE,UAAU,EACb,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,CAAC,EAAE,UAAU,EACb,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,UAAU,CAAC,CAWrB"}
package/dist/record.js ADDED
@@ -0,0 +1,93 @@
1
+ import { aeadOpen, aeadSeal } from "./aead.js";
2
+ import { concatBytes, u32beBytes, utf8 } from "./bytes.js";
3
+ /**
4
+ * Per-record seal/unseal — the per-item vault primitive, authorizer-side mirror
5
+ * of the Rust crate's `sudp::state::{seal_record, unseal_record}`.
6
+ *
7
+ * Eats bytes, spits bytes: the record's business structure, serialization,
8
+ * id derivation, version comparison, merge, tombstones, and GC are ALL the
9
+ * caller's concern. This module only seals one record under the vault key `K`
10
+ * bound — via AEAD associated data — to a structured {@link SealCtx}, so a
11
+ * record can never be opened under a different vault / id / version / domain.
12
+ *
13
+ * The AAD binding detects cross-vault splicing, cross-id substitution,
14
+ * version/ciphertext mismatch, and cross-domain confusion. It does NOT by
15
+ * itself prevent rollback (an attacker substituting a strictly older but
16
+ * validly-sealed record with its matching older `version`) — keep a monotonic
17
+ * per-id version store on the caller side, and put richer causal/merge metadata
18
+ * (HLC, device id, …) INSIDE the plaintext so the AEAD authenticates it too.
19
+ *
20
+ * Every byte layout here (canonical AAD, HKDF info, sealed framing) MUST stay
21
+ * byte-for-byte aligned with `custodian/rust/src/state/record.rs`; the
22
+ * conformance vectors in `test/conformance.test.ts` are pinned against the Rust
23
+ * `assert_eq!` anchors.
24
+ */
25
+ /** Per-record (per-item) domain-separation label. */
26
+ export const DS_ITEM = utf8("sudp/v1/item");
27
+ /** Suite/format tag: `0x01` = XChaCha20-Poly1305 + canonical AAD v1. */
28
+ export const RECORD_SUITE_XCHACHA20POLY1305 = 0x01;
29
+ /** Length-prefixed field: `len_be(u32) ‖ bytes`. */
30
+ function lp(field) {
31
+ return concatBytes(u32beBytes(field.byteLength), field);
32
+ }
33
+ /**
34
+ * Canonical, length-prefixed associated data for a per-record seal:
35
+ *
36
+ * AAD = suite(1) ‖ lp(DS_ITEM) ‖ lp(domain_utf8) ‖ lp(vault) ‖ lp(id) ‖ lp(version)
37
+ * where lp(x) = len_be(x.length as u32, 4 bytes) ‖ x
38
+ *
39
+ * MUST be byte-identical to the Rust crate's `record_aad`.
40
+ */
41
+ export function recordAad(suite, ctx) {
42
+ return concatBytes(new Uint8Array([suite & 0xff]), lp(DS_ITEM), lp(utf8(ctx.domain)), lp(ctx.vault), lp(ctx.id), lp(ctx.version));
43
+ }
44
+ /**
45
+ * Derive the per-record AEAD key `K_aead` from the vault key `k`:
46
+ *
47
+ * K_aead = HKDF-SHA-256(ikm = k, salt = "", info = DS_ITEM)
48
+ *
49
+ * Domain-separated so `K_aead` never shares raw bytes with the caller's
50
+ * `HMAC_K(name)` record-id derivation. MUST match the Rust crate's
51
+ * `derive_item_key`.
52
+ */
53
+ export async function deriveItemKey(k) {
54
+ const km = await crypto.subtle.importKey("raw", k, "HKDF", false, ["deriveBits"]);
55
+ const bits = await crypto.subtle.deriveBits({
56
+ name: "HKDF",
57
+ hash: "SHA-256",
58
+ salt: new Uint8Array(0),
59
+ info: DS_ITEM,
60
+ }, km, 256);
61
+ return new Uint8Array(bits);
62
+ }
63
+ /**
64
+ * Seal one record. Output layout: `suite(1) ‖ nonce(24) ‖ ciphertext ‖ tag(16)`.
65
+ * A fresh random 24-byte nonce is generated per call (Web Crypto).
66
+ */
67
+ export async function sealRecord(k, ctx, plaintext) {
68
+ const kAead = await deriveItemKey(k);
69
+ const aad = recordAad(RECORD_SUITE_XCHACHA20POLY1305, ctx);
70
+ const sealed = aeadSeal(kAead, plaintext, aad); // nonce ‖ ct ‖ tag
71
+ const out = new Uint8Array(1 + sealed.byteLength);
72
+ out[0] = RECORD_SUITE_XCHACHA20POLY1305;
73
+ out.set(sealed, 1);
74
+ return out;
75
+ }
76
+ /**
77
+ * Unseal one record. Throws on any authentication failure (AAD mismatch and
78
+ * tag mismatch are indistinguishable — the same Poly1305 check) or on a
79
+ * structurally malformed blob (empty / unknown suite tag).
80
+ */
81
+ export async function unsealRecord(k, ctx, sealed) {
82
+ if (sealed.byteLength < 1) {
83
+ throw new Error("unsealRecord: empty sealed blob");
84
+ }
85
+ const suite = sealed[0];
86
+ if (suite !== RECORD_SUITE_XCHACHA20POLY1305) {
87
+ throw new Error("unsealRecord: unknown suite tag");
88
+ }
89
+ const kAead = await deriveItemKey(k);
90
+ const aad = recordAad(suite, ctx);
91
+ return aeadOpen(kAead, sealed.slice(1), aad);
92
+ }
93
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudp-protocol/authorizer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Authorizer-side primitives for SUDP (Secret-Use Delegation Protocol): canonical JSON, β computation, wrapping-key derivation, AEAD-as-wrap, plus a WebAuthn adapter under the ./webauthn subpath.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Miracle <xhyumiracle@gmail.com>",
package/src/aad.ts CHANGED
@@ -15,7 +15,7 @@ export const WRAP_VERSION = 0x0001;
15
15
  * DS_WRAP ‖ credentialId ‖ ver_be(u16, big-endian)
16
16
  *
17
17
  * Bound as associated data when sealing/opening `K̂_c` under `W_c`, so a
18
- * peer-wrapped record cannot be substituted across credentials or versions.
18
+ * per-credential-wrapped record cannot be substituted across credentials or versions.
19
19
  */
20
20
  export function wrapBindingAd(
21
21
  credentialId: Uint8Array,
package/src/binding.ts CHANGED
@@ -31,3 +31,24 @@ export async function computeBinding(
31
31
  const opHash = await sha256(canonicalize(op));
32
32
  return sha256(concatBytes(domain, r, opHash));
33
33
  }
34
+
35
+ /**
36
+ * Batch counterpart of {@link computeBinding}:
37
+ *
38
+ * β = SHA-256(domain ‖ r ‖ SHA-256(canonical(ops)))
39
+ *
40
+ * where `ops` is a JSON array of operations. Byte-aligned with the Rust
41
+ * crate's `compute_beta_from_canonical(domain, r, &BatchOperations(ops).canonical_bytes())`.
42
+ *
43
+ * Semantically identical to `computeBinding(domain, r, ops)` because the
44
+ * canonical encoder treats arrays uniformly, but named separately so the
45
+ * "batch" intent is explicit at the call site (and so a single conformance
46
+ * vector pins the batch shape independently).
47
+ */
48
+ export async function computeBatchBinding(
49
+ domain: Uint8Array,
50
+ r: Uint8Array,
51
+ ops: readonly unknown[],
52
+ ): Promise<Uint8Array> {
53
+ return computeBinding(domain, r, ops);
54
+ }
package/src/bytes.ts CHANGED
@@ -23,6 +23,31 @@ export function u16beBytes(n: number): Uint8Array {
23
23
  return new Uint8Array([(n >> 8) & 0xff, n & 0xff]);
24
24
  }
25
25
 
26
+ export function u32beBytes(n: number): Uint8Array {
27
+ if (!Number.isInteger(n) || n < 0 || n > 0xffffffff) {
28
+ throw new Error(`u32beBytes: out of range: ${n}`);
29
+ }
30
+ return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
31
+ }
32
+
33
+ /**
34
+ * 8-byte big-endian encoding of a `u64`. Takes a `bigint` so the full unsigned
35
+ * 64-bit range is exact (a `number` is only safe to 2^53). MUST match the Rust
36
+ * crate's `u64::to_be_bytes`.
37
+ */
38
+ export function u64beBytes(n: bigint): Uint8Array {
39
+ if (n < 0n || n > 0xffffffffffffffffn) {
40
+ throw new Error(`u64beBytes: out of range: ${n}`);
41
+ }
42
+ const out = new Uint8Array(8);
43
+ let v = n;
44
+ for (let i = 7; i >= 0; i--) {
45
+ out[i] = Number(v & 0xffn);
46
+ v >>= 8n;
47
+ }
48
+ return out;
49
+ }
50
+
26
51
  const B64URL_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
27
52
  const B64URL_LOOKUP = (() => {
28
53
  const t = new Int8Array(256).fill(-1);
package/src/index.ts CHANGED
@@ -15,13 +15,24 @@ export {
15
15
  utf8,
16
16
  concatBytes,
17
17
  u16beBytes,
18
+ u32beBytes,
19
+ u64beBytes,
18
20
  bytesToB64Url,
19
21
  b64UrlToBytes,
20
22
  } from "./bytes.js";
21
23
 
22
24
  export { canonicalize } from "./canonical.js";
23
25
  export { sha256 } from "./hash.js";
24
- export { computeBinding, DS_BIND } from "./binding.js";
26
+ export { computeBatchBinding, computeBinding, DS_BIND } from "./binding.js";
25
27
  export { deriveWrappingKey } from "./kdf.js";
26
28
  export { wrapBindingAd, sealAd, DS_WRAP, DS_SEAL, WRAP_VERSION } from "./aad.js";
27
29
  export { aeadEncrypt, aeadSeal, aeadOpen } from "./aead.js";
30
+ export {
31
+ recordAad,
32
+ deriveItemKey,
33
+ sealRecord,
34
+ unsealRecord,
35
+ DS_ITEM,
36
+ RECORD_SUITE_XCHACHA20POLY1305,
37
+ type SealCtx,
38
+ } from "./record.js";
package/src/record.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { aeadOpen, aeadSeal } from "./aead.js";
2
+ import { concatBytes, u32beBytes, utf8 } from "./bytes.js";
3
+
4
+ /**
5
+ * Per-record seal/unseal — the per-item vault primitive, authorizer-side mirror
6
+ * of the Rust crate's `sudp::state::{seal_record, unseal_record}`.
7
+ *
8
+ * Eats bytes, spits bytes: the record's business structure, serialization,
9
+ * id derivation, version comparison, merge, tombstones, and GC are ALL the
10
+ * caller's concern. This module only seals one record under the vault key `K`
11
+ * bound — via AEAD associated data — to a structured {@link SealCtx}, so a
12
+ * record can never be opened under a different vault / id / version / domain.
13
+ *
14
+ * The AAD binding detects cross-vault splicing, cross-id substitution,
15
+ * version/ciphertext mismatch, and cross-domain confusion. It does NOT by
16
+ * itself prevent rollback (an attacker substituting a strictly older but
17
+ * validly-sealed record with its matching older `version`) — keep a monotonic
18
+ * per-id version store on the caller side, and put richer causal/merge metadata
19
+ * (HLC, device id, …) INSIDE the plaintext so the AEAD authenticates it too.
20
+ *
21
+ * Every byte layout here (canonical AAD, HKDF info, sealed framing) MUST stay
22
+ * byte-for-byte aligned with `custodian/rust/src/state/record.rs`; the
23
+ * conformance vectors in `test/conformance.test.ts` are pinned against the Rust
24
+ * `assert_eq!` anchors.
25
+ */
26
+
27
+ /** Per-record (per-item) domain-separation label. */
28
+ export const DS_ITEM = utf8("sudp/v1/item");
29
+
30
+ /** Suite/format tag: `0x01` = XChaCha20-Poly1305 + canonical AAD v1. */
31
+ export const RECORD_SUITE_XCHACHA20POLY1305 = 0x01;
32
+
33
+ /**
34
+ * Structured sealing context for one record. sudp builds the AEAD associated
35
+ * data from these fields; callers MUST NOT hand-assemble opaque AAD.
36
+ */
37
+ export interface SealCtx {
38
+ /** Purpose separation within the per-record domain, e.g. `"item"` / `"keyset"`. */
39
+ domain: string;
40
+ /** Vault identifier (anti cross-vault splice). */
41
+ vault: Uint8Array;
42
+ /** Opaque record id — typically `HMAC_K(name)`; never interpreted (anti X-as-Y). */
43
+ id: Uint8Array;
44
+ /**
45
+ * Opaque, monotonic-per-id ordering value. sudp binds it into the AAD but
46
+ * never interprets it — meaning (server sequence number, Lamport/HLC stamp,
47
+ * version-vector, …) and the whole conflict-resolution strategy are the
48
+ * caller's. Recommended scalar encoding for the common case: `u64`
49
+ * big-endian (`u64beBytes(n)`). Binds the ciphertext to a version (detects
50
+ * mismatch, not rollback).
51
+ */
52
+ version: Uint8Array;
53
+ }
54
+
55
+ /** Length-prefixed field: `len_be(u32) ‖ bytes`. */
56
+ function lp(field: Uint8Array): Uint8Array {
57
+ return concatBytes(u32beBytes(field.byteLength), field);
58
+ }
59
+
60
+ /**
61
+ * Canonical, length-prefixed associated data for a per-record seal:
62
+ *
63
+ * AAD = suite(1) ‖ lp(DS_ITEM) ‖ lp(domain_utf8) ‖ lp(vault) ‖ lp(id) ‖ lp(version)
64
+ * where lp(x) = len_be(x.length as u32, 4 bytes) ‖ x
65
+ *
66
+ * MUST be byte-identical to the Rust crate's `record_aad`.
67
+ */
68
+ export function recordAad(suite: number, ctx: SealCtx): Uint8Array {
69
+ return concatBytes(
70
+ new Uint8Array([suite & 0xff]),
71
+ lp(DS_ITEM),
72
+ lp(utf8(ctx.domain)),
73
+ lp(ctx.vault),
74
+ lp(ctx.id),
75
+ lp(ctx.version),
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Derive the per-record AEAD key `K_aead` from the vault key `k`:
81
+ *
82
+ * K_aead = HKDF-SHA-256(ikm = k, salt = "", info = DS_ITEM)
83
+ *
84
+ * Domain-separated so `K_aead` never shares raw bytes with the caller's
85
+ * `HMAC_K(name)` record-id derivation. MUST match the Rust crate's
86
+ * `derive_item_key`.
87
+ */
88
+ export async function deriveItemKey(k: Uint8Array): Promise<Uint8Array> {
89
+ const km = await crypto.subtle.importKey(
90
+ "raw",
91
+ k as unknown as ArrayBuffer,
92
+ "HKDF",
93
+ false,
94
+ ["deriveBits"],
95
+ );
96
+ const bits = await crypto.subtle.deriveBits(
97
+ {
98
+ name: "HKDF",
99
+ hash: "SHA-256",
100
+ salt: new Uint8Array(0) as unknown as ArrayBuffer,
101
+ info: DS_ITEM as unknown as ArrayBuffer,
102
+ },
103
+ km,
104
+ 256,
105
+ );
106
+ return new Uint8Array(bits);
107
+ }
108
+
109
+ /**
110
+ * Seal one record. Output layout: `suite(1) ‖ nonce(24) ‖ ciphertext ‖ tag(16)`.
111
+ * A fresh random 24-byte nonce is generated per call (Web Crypto).
112
+ */
113
+ export async function sealRecord(
114
+ k: Uint8Array,
115
+ ctx: SealCtx,
116
+ plaintext: Uint8Array,
117
+ ): Promise<Uint8Array> {
118
+ const kAead = await deriveItemKey(k);
119
+ const aad = recordAad(RECORD_SUITE_XCHACHA20POLY1305, ctx);
120
+ const sealed = aeadSeal(kAead, plaintext, aad); // nonce ‖ ct ‖ tag
121
+ const out = new Uint8Array(1 + sealed.byteLength);
122
+ out[0] = RECORD_SUITE_XCHACHA20POLY1305;
123
+ out.set(sealed, 1);
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Unseal one record. Throws on any authentication failure (AAD mismatch and
129
+ * tag mismatch are indistinguishable — the same Poly1305 check) or on a
130
+ * structurally malformed blob (empty / unknown suite tag).
131
+ */
132
+ export async function unsealRecord(
133
+ k: Uint8Array,
134
+ ctx: SealCtx,
135
+ sealed: Uint8Array,
136
+ ): Promise<Uint8Array> {
137
+ if (sealed.byteLength < 1) {
138
+ throw new Error("unsealRecord: empty sealed blob");
139
+ }
140
+ const suite = sealed[0]!;
141
+ if (suite !== RECORD_SUITE_XCHACHA20POLY1305) {
142
+ throw new Error("unsealRecord: unknown suite tag");
143
+ }
144
+ const kAead = await deriveItemKey(k);
145
+ const aad = recordAad(suite, ctx);
146
+ return aeadOpen(kAead, sealed.slice(1), aad);
147
+ }