@interop/was-client 0.17.0 → 0.19.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 +88 -0
- package/dist/Collection.d.ts +1 -46
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +77 -79
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +1 -51
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +61 -58
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts +1 -27
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +65 -65
- package/dist/Space.js.map +1 -1
- package/dist/WasClient.d.ts +1 -12
- package/dist/WasClient.d.ts.map +1 -1
- package/dist/WasClient.js +15 -15
- package/dist/WasClient.js.map +1 -1
- package/dist/codec.d.ts +15 -2
- package/dist/codec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.d.ts +13 -95
- package/dist/edv/EdvCodec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +221 -52
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts +1 -53
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +29 -29
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/edv/docCipher.d.ts +111 -0
- package/dist/edv/docCipher.d.ts.map +1 -0
- package/dist/edv/docCipher.js +236 -0
- package/dist/edv/docCipher.js.map +1 -0
- package/dist/edv/epochKeys.d.ts.map +1 -1
- package/dist/edv/epochKeys.js +46 -16
- package/dist/edv/epochKeys.js.map +1 -1
- package/dist/edv/epochMac.d.ts +32 -0
- package/dist/edv/epochMac.d.ts.map +1 -0
- package/dist/edv/epochMac.js +117 -0
- package/dist/edv/epochMac.js.map +1 -0
- package/dist/edv/index.d.ts +2 -0
- package/dist/edv/index.d.ts.map +1 -1
- package/dist/edv/index.js +1 -0
- package/dist/edv/index.js.map +1 -1
- package/dist/edv/recipients.d.ts.map +1 -1
- package/dist/edv/recipients.js +26 -3
- package/dist/edv/recipients.js.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codec.d.ts +1 -2
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +10 -10
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/features.d.ts +1 -12
- package/dist/internal/features.d.ts.map +1 -1
- package/dist/internal/features.js +8 -8
- package/dist/internal/features.js.map +1 -1
- package/dist/internal/pagination.d.ts +1 -1
- package/dist/internal/pagination.js +1 -1
- package/dist/sync/cid.d.ts +31 -0
- package/dist/sync/cid.d.ts.map +1 -0
- package/dist/sync/cid.js +55 -0
- package/dist/sync/cid.js.map +1 -0
- package/dist/sync/envelope.d.ts +20 -0
- package/dist/sync/envelope.d.ts.map +1 -0
- package/dist/sync/envelope.js +17 -0
- package/dist/sync/envelope.js.map +1 -0
- package/dist/sync/index.d.ts +29 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +28 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/plaintextCipher.d.ts +16 -0
- package/dist/sync/plaintextCipher.d.ts.map +1 -0
- package/dist/sync/plaintextCipher.js +36 -0
- package/dist/sync/plaintextCipher.js.map +1 -0
- package/dist/sync/port.d.ts +78 -0
- package/dist/sync/port.d.ts.map +1 -0
- package/dist/sync/port.js +204 -0
- package/dist/sync/port.js.map +1 -0
- package/dist/sync/provisioning.d.ts +41 -0
- package/dist/sync/provisioning.d.ts.map +1 -0
- package/dist/sync/provisioning.js +44 -0
- package/dist/sync/provisioning.js.map +1 -0
- package/dist/sync/types.d.ts +166 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -5
package/dist/edv/EdvCodec.d.ts
CHANGED
|
@@ -9,21 +9,8 @@ import type { Json, ResourceData, ResourceMetadataCustom } from '../types.js';
|
|
|
9
9
|
* collection handle.
|
|
10
10
|
*/
|
|
11
11
|
export declare class EdvCodec implements ResourceCodec {
|
|
12
|
+
#private;
|
|
12
13
|
readonly conditionalWrites = true;
|
|
13
|
-
private readonly _edv;
|
|
14
|
-
private readonly _writeKey;
|
|
15
|
-
/**
|
|
16
|
-
* The static JWE recipient descriptor every write encrypts to (the write
|
|
17
|
-
* key's `{ kid, alg }` header; the ephemeral `epk` is minted later inside
|
|
18
|
-
* `encrypt`). Deterministic per codec, so computed once here instead of per
|
|
19
|
-
* write.
|
|
20
|
-
*/
|
|
21
|
-
private readonly _recipients;
|
|
22
|
-
private readonly _readKeys;
|
|
23
|
-
private readonly _writeEpoch?;
|
|
24
|
-
private readonly _contentType;
|
|
25
|
-
private readonly _maxBlobBytes;
|
|
26
|
-
private readonly _idDerivation;
|
|
27
14
|
/**
|
|
28
15
|
* @param options {object}
|
|
29
16
|
* @param options.edv {EdvClientCore} holds the cipher + key resolver
|
|
@@ -44,8 +31,13 @@ export declare class EdvCodec implements ResourceCodec {
|
|
|
44
31
|
* @param options.idDerivation {string} how `add()` mints a document
|
|
45
32
|
* id: `'random'` (classic `generateId()`) or `'content'` (derived from the
|
|
46
33
|
* JWE ciphertext, content-addressed)
|
|
34
|
+
* @param [options.version] {number} the EDV-over-WAS scheme version to bind
|
|
35
|
+
* into each envelope's `was.v` (defaults to `1`)
|
|
36
|
+
* @param [options.hasEpochs] {boolean} whether the collection has key
|
|
37
|
+
* epochs, so a decoded envelope's `was.epoch` binding is checked against the
|
|
38
|
+
* decrypting key's epoch (defaults to `false`)
|
|
47
39
|
*/
|
|
48
|
-
constructor({ edv, keyAgreementKey, readKeys, writeEpoch, contentType, maxBlobBytes, idDerivation }: {
|
|
40
|
+
constructor({ edv, keyAgreementKey, readKeys, writeEpoch, contentType, maxBlobBytes, idDerivation, version, hasEpochs }: {
|
|
49
41
|
edv: EdvClientCore;
|
|
50
42
|
keyAgreementKey: IKeyAgreementKey;
|
|
51
43
|
readKeys?: IKeyAgreementKey[];
|
|
@@ -53,6 +45,8 @@ export declare class EdvCodec implements ResourceCodec {
|
|
|
53
45
|
contentType: string;
|
|
54
46
|
maxBlobBytes: number;
|
|
55
47
|
idDerivation: 'random' | 'content';
|
|
48
|
+
version?: number;
|
|
49
|
+
hasEpochs?: boolean;
|
|
56
50
|
});
|
|
57
51
|
/**
|
|
58
52
|
* @inheritdoc
|
|
@@ -69,23 +63,7 @@ export declare class EdvCodec implements ResourceCodec {
|
|
|
69
63
|
decode(response: {
|
|
70
64
|
data?: unknown;
|
|
71
65
|
json(): Promise<unknown>;
|
|
72
|
-
}): Promise<Json | Blob>;
|
|
73
|
-
/**
|
|
74
|
-
* Decrypts a stored EDV envelope, selecting which read key to use by matching
|
|
75
|
-
* the envelope's JWE recipient `kid` against this reader's candidate keys
|
|
76
|
-
* (one per epoch it can unwrap). On a single-key collection there is exactly
|
|
77
|
-
* one candidate; on a multi-recipient collection a resource written under an
|
|
78
|
-
* older epoch selects that epoch's key, so history stays readable.
|
|
79
|
-
*
|
|
80
|
-
* A stored envelope naming an epoch this reader holds no key for (it was never
|
|
81
|
-
* a recipient of that epoch, or was removed and the epoch rotated) fails with
|
|
82
|
-
* {@link KeyUnwrapError} -- the **read** axis only; it says nothing about
|
|
83
|
-
* whether the server will still serve (pull) the ciphertext.
|
|
84
|
-
*
|
|
85
|
-
* @param encryptedDoc {IEncryptedDocument}
|
|
86
|
-
* @returns {Promise<{ content?: unknown; meta?: Record<string, unknown> }>}
|
|
87
|
-
*/
|
|
88
|
-
private _decrypt;
|
|
66
|
+
}, expectedId?: string): Promise<Json | Blob>;
|
|
89
67
|
/**
|
|
90
68
|
* @inheritdoc
|
|
91
69
|
*
|
|
@@ -95,8 +73,9 @@ export declare class EdvCodec implements ResourceCodec {
|
|
|
95
73
|
* inert (metadata concurrency is the server's plaintext `metaVersion`, not the
|
|
96
74
|
* envelope), so each write re-encrypts fresh with no `update`.
|
|
97
75
|
*/
|
|
98
|
-
encodeMeta({ custom }: {
|
|
76
|
+
encodeMeta({ custom, id: resourceId }: {
|
|
99
77
|
custom: ResourceMetadataCustom;
|
|
78
|
+
id?: string;
|
|
100
79
|
}): Promise<{
|
|
101
80
|
custom: object;
|
|
102
81
|
}>;
|
|
@@ -110,68 +89,7 @@ export declare class EdvCodec implements ResourceCodec {
|
|
|
110
89
|
*/
|
|
111
90
|
decodeMeta({ custom }: {
|
|
112
91
|
custom?: unknown;
|
|
113
|
-
}): Promise<ResourceMetadataCustom>;
|
|
114
|
-
/**
|
|
115
|
-
* Asserts that a document read from an encrypted collection is an EDV envelope
|
|
116
|
-
* (`{ jwe, ... }`) before it is handed to the cipher. A plaintext or foreign
|
|
117
|
-
* resource -- one written without this codec -- carries no `jwe`, which would
|
|
118
|
-
* otherwise make the EDV core throw a raw `TypeError`. Surfacing a typed
|
|
119
|
-
* `EncryptionError` keeps the fail-closed contract legible to callers.
|
|
120
|
-
*
|
|
121
|
-
* For an `update`, the envelope's `sequence` is also validated: the cipher
|
|
122
|
-
* requires a non-negative safe integer to advance from, so a foreign envelope
|
|
123
|
-
* without one (or with a malformed one) must fail here as a typed
|
|
124
|
-
* `EncryptionError` rather than as the cipher's raw `Error`.
|
|
125
|
-
*
|
|
126
|
-
* @param doc {unknown}
|
|
127
|
-
* @param context {string} the operation in progress (`read` / `update`),
|
|
128
|
-
* for the message
|
|
129
|
-
* @returns {asserts doc is IEncryptedDocument}
|
|
130
|
-
*/
|
|
131
|
-
private _assertEnvelope;
|
|
132
|
-
/**
|
|
133
|
-
* Splits a caller value into a decrypted EDV document `{ content, meta }`,
|
|
134
|
-
* carrying the plaintext content type and inline-encoding discriminator in
|
|
135
|
-
* `meta`. Three cases:
|
|
136
|
-
*
|
|
137
|
-
* 1. JSON object/array to `content` verbatim, `meta = { contentType }` (no
|
|
138
|
-
* `encoding`); the shape of `content` is never inspected on read, so a
|
|
139
|
-
* caller object shaped like `{ text }` / `{ bytes }` round-trips as itself.
|
|
140
|
-
* 2. Text (`Blob`/`Uint8Array` of a text-family type that is valid UTF-8)
|
|
141
|
-
* to `content = { text }`, `meta = { contentType, encoding: 'utf-8' }`;
|
|
142
|
-
* stored legibly with no base64 inflation.
|
|
143
|
-
* 3. Binary (any other `Blob`/`Uint8Array`) to `content = { bytes: base64 }`,
|
|
144
|
-
* `meta = { contentType, encoding: 'base64' }`.
|
|
145
|
-
*
|
|
146
|
-
* A bare primitive is rejected (mirroring the plaintext `prepareBody`
|
|
147
|
-
* contract). The binary/text detection and content-type precedence are the
|
|
148
|
-
* shared `resolvePayload` rules, so the plaintext and encrypted write paths
|
|
149
|
-
* cannot drift.
|
|
150
|
-
*
|
|
151
|
-
* @param data {ResourceData}
|
|
152
|
-
* @param [contentType] {string} caller-supplied content type
|
|
153
|
-
* @param [id] {string} resource id, for the extension guess
|
|
154
|
-
* @returns {Promise<{ content: Record<string, unknown>; meta:
|
|
155
|
-
* Record<string, unknown> }>}
|
|
156
|
-
*/
|
|
157
|
-
private _toDocument;
|
|
158
|
-
/**
|
|
159
|
-
* Reconstructs a caller value from a decrypted EDV document, discriminating
|
|
160
|
-
* on `meta.encoding`:
|
|
161
|
-
*
|
|
162
|
-
* - `'utf-8'` to a `Blob` typed `meta.contentType` from `content.text`.
|
|
163
|
-
* - `'base64'` to a `Blob` typed `meta.contentType` from `content.bytes`.
|
|
164
|
-
* - absent (or `meta` absent) to `content` returned verbatim as JSON.
|
|
165
|
-
*
|
|
166
|
-
* A malformed inner shape (an encoding that does not match its container key's
|
|
167
|
-
* type) throws {@link EncryptionError} -- the decrypted-document analogue of
|
|
168
|
-
* `_assertEnvelope`'s outer guard.
|
|
169
|
-
*
|
|
170
|
-
* @param content {unknown}
|
|
171
|
-
* @param [meta] {Record<string, unknown>}
|
|
172
|
-
* @returns {Json | Blob}
|
|
173
|
-
*/
|
|
174
|
-
private _fromDocument;
|
|
92
|
+
}, expectedId?: string): Promise<ResourceMetadataCustom>;
|
|
175
93
|
}
|
|
176
94
|
/**
|
|
177
95
|
* The per-collection key material an EDV codec is built from.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EdvCodec.d.ts","sourceRoot":"","sources":["../../src/edv/EdvCodec.ts"],"names":[],"mappings":"AAuDA,OAAO,EAAE,aAAa,EAAe,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EAEb,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,aAAa,CAAA;AAiBpB,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAyD7E;;;;GAIG;AACH,qBAAa,QAAS,YAAW,aAAa
|
|
1
|
+
{"version":3,"file":"EdvCodec.d.ts","sourceRoot":"","sources":["../../src/edv/EdvCodec.ts"],"names":[],"mappings":"AAuDA,OAAO,EAAE,aAAa,EAAe,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,KAAK,EAEV,gBAAgB,EAChB,YAAY,EAEb,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,aAAa,CAAA;AAiBpB,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAyD7E;;;;GAIG;AACH,qBAAa,QAAS,YAAW,aAAa;;IAC5C,QAAQ,CAAC,iBAAiB,QAAO;IA4BjC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;gBACS,EACV,GAAG,EACH,eAAe,EACf,QAAQ,EACR,UAAU,EACV,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,OAAO,EACP,SAAS,EACV,EAAE;QACD,GAAG,EAAE,aAAa,CAAA;QAClB,eAAe,EAAE,gBAAgB,CAAA;QACjC,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;QAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,YAAY,EAAE,QAAQ,GAAG,SAAS,CAAA;QAClC,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,OAAO,CAAA;KACpB;IAaD;;OAEG;IACG,MAAM,CAAC,EACX,EAAE,EACF,IAAI,EACJ,WAAW,EACX,OAAO,EACR,EAAE;QACD,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,YAAY,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;KAC9B,GAAG,OAAO,CAAC,YAAY,CAAC;IA0GzB;;OAEG;IACG,MAAM,CACV,QAAQ,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;KACzB,EACD,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAwLvB;;;;;;;;OAQG;IACG,UAAU,CAAC,EACf,MAAM,EACN,EAAE,EAAE,UAAU,EACf,EAAE;QACD,MAAM,EAAE,sBAAsB,CAAA;QAC9B,EAAE,CAAC,EAAE,MAAM,CAAA;KACZ,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA0B/B;;;;;;;OAOG;IACG,UAAU,CACd,EACE,MAAM,EACP,EAAE;QACD,MAAM,CAAC,EAAE,OAAO,CAAA;KACjB,EACD,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,sBAAsB,CAAC;CAkMnC;AAoDD;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,WAAkC,EAClC,YAAqC,EACrC,YAAuB,EACxB,EAAE;IACD,WAAW,EAAE,CAAC,GAAG,EAAE;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;KACrB,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CACpC,GAAG,kBAAkB,CA+DrB"}
|
package/dist/edv/EdvCodec.js
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* plaintext `name`/`tags`. A reader with the keys decrypts it back
|
|
53
53
|
* transparently via `meta()`.
|
|
54
54
|
*/
|
|
55
|
-
import { base64 } from '@scure/base';
|
|
55
|
+
import { base64, base64urlnopad } from '@scure/base';
|
|
56
56
|
import { EdvClientCore, assertDocId } from '@interop/edv-client';
|
|
57
57
|
import { EncryptionError, IntegrityError, KeyUnwrapError, ValidationError } from '../errors.js';
|
|
58
58
|
import { readEtag } from '../internal/conditional.js';
|
|
@@ -116,20 +116,31 @@ function isKeyMiss(err) {
|
|
|
116
116
|
*/
|
|
117
117
|
export class EdvCodec {
|
|
118
118
|
conditionalWrites = true;
|
|
119
|
-
|
|
120
|
-
_writeKey;
|
|
119
|
+
#edv;
|
|
121
120
|
/**
|
|
122
121
|
* The static JWE recipient descriptor every write encrypts to (the write
|
|
123
122
|
* key's `{ kid, alg }` header; the ephemeral `epk` is minted later inside
|
|
124
123
|
* `encrypt`). Deterministic per codec, so computed once here instead of per
|
|
125
124
|
* write.
|
|
126
125
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
#recipients;
|
|
127
|
+
#readKeys;
|
|
128
|
+
#writeEpoch;
|
|
129
|
+
#contentType;
|
|
130
|
+
#maxBlobBytes;
|
|
131
|
+
#idDerivation;
|
|
132
|
+
/**
|
|
133
|
+
* The EDV-over-WAS scheme version this codec binds into every envelope's
|
|
134
|
+
* `was.v` protected-header parameter (the marker's `version`, `1` when
|
|
135
|
+
* absent). Read side rejects an envelope stamped with a greater version.
|
|
136
|
+
*/
|
|
137
|
+
#version;
|
|
138
|
+
/**
|
|
139
|
+
* Whether this codec's collection has key epochs. When it does, an envelope's
|
|
140
|
+
* `was.epoch` binding is checked on decode against the epoch of the key that
|
|
141
|
+
* actually decrypted it.
|
|
142
|
+
*/
|
|
143
|
+
#hasEpochs;
|
|
133
144
|
/**
|
|
134
145
|
* @param options {object}
|
|
135
146
|
* @param options.edv {EdvClientCore} holds the cipher + key resolver
|
|
@@ -150,17 +161,23 @@ export class EdvCodec {
|
|
|
150
161
|
* @param options.idDerivation {string} how `add()` mints a document
|
|
151
162
|
* id: `'random'` (classic `generateId()`) or `'content'` (derived from the
|
|
152
163
|
* JWE ciphertext, content-addressed)
|
|
164
|
+
* @param [options.version] {number} the EDV-over-WAS scheme version to bind
|
|
165
|
+
* into each envelope's `was.v` (defaults to `1`)
|
|
166
|
+
* @param [options.hasEpochs] {boolean} whether the collection has key
|
|
167
|
+
* epochs, so a decoded envelope's `was.epoch` binding is checked against the
|
|
168
|
+
* decrypting key's epoch (defaults to `false`)
|
|
153
169
|
*/
|
|
154
|
-
constructor({ edv, keyAgreementKey, readKeys, writeEpoch, contentType, maxBlobBytes, idDerivation }) {
|
|
155
|
-
this
|
|
156
|
-
this
|
|
157
|
-
this._recipients =
|
|
170
|
+
constructor({ edv, keyAgreementKey, readKeys, writeEpoch, contentType, maxBlobBytes, idDerivation, version, hasEpochs }) {
|
|
171
|
+
this.#edv = edv;
|
|
172
|
+
this.#recipients =
|
|
158
173
|
edv.documentCipher.createDefaultRecipients(keyAgreementKey);
|
|
159
|
-
this
|
|
160
|
-
this
|
|
161
|
-
this
|
|
162
|
-
this
|
|
163
|
-
this
|
|
174
|
+
this.#readKeys = readKeys ?? [keyAgreementKey];
|
|
175
|
+
this.#writeEpoch = writeEpoch;
|
|
176
|
+
this.#contentType = contentType;
|
|
177
|
+
this.#maxBlobBytes = maxBlobBytes;
|
|
178
|
+
this.#idDerivation = idDerivation;
|
|
179
|
+
this.#version = version ?? 1;
|
|
180
|
+
this.#hasEpochs = hasEpochs ?? false;
|
|
164
181
|
}
|
|
165
182
|
/**
|
|
166
183
|
* @inheritdoc
|
|
@@ -184,10 +201,10 @@ export class EdvCodec {
|
|
|
184
201
|
// mode -- leave it unset and stamp the content-derived id after encryption
|
|
185
202
|
// (the id is a function of the ciphertext, which does not exist yet).
|
|
186
203
|
let docId = id ??
|
|
187
|
-
(this
|
|
204
|
+
(this.#idDerivation === 'content'
|
|
188
205
|
? undefined
|
|
189
|
-
: (await this.
|
|
190
|
-
const { content, meta } = await this
|
|
206
|
+
: (await this.#edv.generateId()));
|
|
207
|
+
const { content, meta } = await this.#toDocument(data, contentType, docId);
|
|
191
208
|
// When the write path pre-read a current envelope, advance `sequence` from
|
|
192
209
|
// its prior value (`encrypt({ update: true })` increments it) and pin the
|
|
193
210
|
// write to the server's current ETag with `If-Match`. With no prior envelope
|
|
@@ -196,10 +213,25 @@ export class EdvCodec {
|
|
|
196
213
|
let priorDoc = null;
|
|
197
214
|
if (current) {
|
|
198
215
|
const read = await readJsonData(current);
|
|
199
|
-
this
|
|
216
|
+
this.#assertEnvelope(read, 'update');
|
|
200
217
|
priorDoc = read;
|
|
201
218
|
}
|
|
202
|
-
const { documentCipher } = this
|
|
219
|
+
const { documentCipher } = this.#edv;
|
|
220
|
+
// Bind an AEAD-authenticated `was` parameter into the JWE protected header:
|
|
221
|
+
// the scheme version, the resource id when known at encrypt time (omitted
|
|
222
|
+
// for a content-derived id, which does not exist until after encryption),
|
|
223
|
+
// and the write epoch on a multi-recipient collection. A server that swaps
|
|
224
|
+
// two envelopes between ids (or replays one under a rolled-back epoch) is
|
|
225
|
+
// then detected on decrypt.
|
|
226
|
+
const was = {
|
|
227
|
+
v: this.#version
|
|
228
|
+
};
|
|
229
|
+
if (docId !== undefined) {
|
|
230
|
+
was.resource = docId;
|
|
231
|
+
}
|
|
232
|
+
if (this.#writeEpoch !== undefined) {
|
|
233
|
+
was.epoch = this.#writeEpoch;
|
|
234
|
+
}
|
|
203
235
|
const encrypted = await documentCipher.encrypt({
|
|
204
236
|
doc: {
|
|
205
237
|
...(docId !== undefined && { id: docId }),
|
|
@@ -210,10 +242,11 @@ export class EdvCodec {
|
|
|
210
242
|
meta,
|
|
211
243
|
...(priorDoc && { sequence: priorDoc.sequence })
|
|
212
244
|
},
|
|
213
|
-
recipients: this
|
|
214
|
-
keyResolver: this.
|
|
245
|
+
recipients: this.#recipients,
|
|
246
|
+
keyResolver: this.#edv.keyResolver,
|
|
215
247
|
hmac: undefined,
|
|
216
|
-
update: priorDoc !== null
|
|
248
|
+
update: priorDoc !== null,
|
|
249
|
+
additionalProtectedParams: { was }
|
|
217
250
|
});
|
|
218
251
|
if (docId === undefined) {
|
|
219
252
|
// Encrypt-then-stamp: the id lives in the cleartext envelope, outside the
|
|
@@ -225,7 +258,7 @@ export class EdvCodec {
|
|
|
225
258
|
return {
|
|
226
259
|
id: docId,
|
|
227
260
|
body: envelopeBytes(encrypted),
|
|
228
|
-
contentType: this
|
|
261
|
+
contentType: this.#contentType,
|
|
229
262
|
// Surface the plaintext content type (the server-opaque envelope type stays
|
|
230
263
|
// `contentType`) so `add()` reports the real resource type.
|
|
231
264
|
resourceContentType: meta.contentType,
|
|
@@ -245,17 +278,22 @@ export class EdvCodec {
|
|
|
245
278
|
// Stamp the key epoch this write encrypted under (the `currentEpoch`), so
|
|
246
279
|
// the server records it and a reader can pick the epoch key. Absent on a
|
|
247
280
|
// single-key collection.
|
|
248
|
-
...(this
|
|
281
|
+
...(this.#writeEpoch !== undefined && { epoch: this.#writeEpoch })
|
|
249
282
|
};
|
|
250
283
|
}
|
|
251
284
|
/**
|
|
252
285
|
* @inheritdoc
|
|
253
286
|
*/
|
|
254
|
-
async decode(response) {
|
|
287
|
+
async decode(response, expectedId) {
|
|
255
288
|
const encryptedDoc = await readJsonData(response);
|
|
256
|
-
this
|
|
257
|
-
const decrypted = await this
|
|
258
|
-
|
|
289
|
+
this.#assertEnvelope(encryptedDoc, 'read');
|
|
290
|
+
const decrypted = await this.#decrypt(encryptedDoc);
|
|
291
|
+
await this.#verifyBinding({
|
|
292
|
+
jwe: encryptedDoc.jwe,
|
|
293
|
+
expectedId,
|
|
294
|
+
keyId: decrypted.keyId
|
|
295
|
+
});
|
|
296
|
+
return this.#fromDocument(decrypted.content, decrypted.meta);
|
|
259
297
|
}
|
|
260
298
|
/**
|
|
261
299
|
* Decrypts a stored EDV envelope, selecting which read key to use by matching
|
|
@@ -269,10 +307,15 @@ export class EdvCodec {
|
|
|
269
307
|
* {@link KeyUnwrapError} -- the **read** axis only; it says nothing about
|
|
270
308
|
* whether the server will still serve (pull) the ciphertext.
|
|
271
309
|
*
|
|
310
|
+
* Also returns the `id` of the key that actually decrypted the envelope (its
|
|
311
|
+
* JWE recipient `kid`), so {@link _verifyBinding} can check a `was.epoch`
|
|
312
|
+
* binding against the epoch of the decrypting key.
|
|
313
|
+
*
|
|
272
314
|
* @param encryptedDoc {IEncryptedDocument}
|
|
273
|
-
* @returns {Promise<{ content?: unknown; meta?: Record<string, unknown
|
|
315
|
+
* @returns {Promise<{ content?: unknown; meta?: Record<string, unknown>;
|
|
316
|
+
* keyId?: string }>}
|
|
274
317
|
*/
|
|
275
|
-
async
|
|
318
|
+
async #decrypt(encryptedDoc) {
|
|
276
319
|
const recipients = encryptedDoc.jwe.recipients ?? [];
|
|
277
320
|
const kids = new Set(recipients
|
|
278
321
|
.map(recipient => recipient.header?.kid)
|
|
@@ -284,14 +327,15 @@ export class EdvCodec {
|
|
|
284
327
|
// exact-match partition is empty even though a candidate can still unwrap it.
|
|
285
328
|
// For a well-formed epoch envelope the exact match always hits, so `rest` is
|
|
286
329
|
// normally unreached.
|
|
287
|
-
const preferred = this.
|
|
288
|
-
const rest = this.
|
|
330
|
+
const preferred = this.#readKeys.filter(key => kids.has(key.id));
|
|
331
|
+
const rest = this.#readKeys.filter(key => !kids.has(key.id));
|
|
289
332
|
for (const keyAgreementKey of [...preferred, ...rest]) {
|
|
290
333
|
try {
|
|
291
|
-
|
|
334
|
+
const decrypted = await this.#edv.documentCipher.decrypt({
|
|
292
335
|
encryptedDoc,
|
|
293
336
|
keyAgreementKey
|
|
294
337
|
});
|
|
338
|
+
return { ...decrypted, keyId: keyAgreementKey.id };
|
|
295
339
|
}
|
|
296
340
|
catch (err) {
|
|
297
341
|
if (isKeyMiss(err)) {
|
|
@@ -318,6 +362,77 @@ export class EdvCodec {
|
|
|
318
362
|
'collection and the epoch was rotated). This is the read axis only -- ' +
|
|
319
363
|
'the server may still serve the ciphertext (a separate zcap decision).');
|
|
320
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Verifies the AEAD-authenticated `was` binding on a successfully-decrypted
|
|
367
|
+
* envelope (spec "Request Body Integrity"'s envelope half). Decrypt success
|
|
368
|
+
* proves the protected header authentic, so this runs only after a decrypt
|
|
369
|
+
* succeeds. Enforces, in order:
|
|
370
|
+
*
|
|
371
|
+
* - No `was` parameter at all: a legacy envelope, accepted unchanged (this
|
|
372
|
+
* client wrote it before the binding existed, or a foreign EDV writer did).
|
|
373
|
+
* - `was.v` greater than this codec's scheme version: a future-scheme envelope
|
|
374
|
+
* this client does not implement -- {@link EncryptionError}.
|
|
375
|
+
* - `was.resource` present and the expected id known: a mismatch is a server-side
|
|
376
|
+
* swap of two resources' envelopes -- {@link IntegrityError}.
|
|
377
|
+
* - `was` present but `resource` absent (a content-derived write) and the expected
|
|
378
|
+
* id known: the envelope's ciphertext must re-derive to the expected id
|
|
379
|
+
* ({@link EdvDocumentCipher.deriveId}); a mismatch means the envelope was
|
|
380
|
+
* copied under a different id -- {@link IntegrityError}. This check is NEVER
|
|
381
|
+
* applied to an envelope with no `was` at all (a legacy random-id envelope
|
|
382
|
+
* would fail it wrongly).
|
|
383
|
+
* - On a collection with epochs, `was.epoch` present: it must equal the epoch
|
|
384
|
+
* (the `did:key` before the `#`) of the key that actually decrypted -- a
|
|
385
|
+
* mismatch is a replay under a different epoch's key -- {@link IntegrityError}.
|
|
386
|
+
*
|
|
387
|
+
* @param options {object}
|
|
388
|
+
* @param options.jwe {unknown} the envelope's JWE (its `protected` header is
|
|
389
|
+
* parsed for `was`)
|
|
390
|
+
* @param [options.expectedId] {string} the resource id the read targeted
|
|
391
|
+
* @param [options.keyId] {string} the id of the key that decrypted, for the
|
|
392
|
+
* epoch check
|
|
393
|
+
* @returns {Promise<void>}
|
|
394
|
+
*/
|
|
395
|
+
async #verifyBinding({ jwe, expectedId, keyId }) {
|
|
396
|
+
const was = parseWasHeader(jwe);
|
|
397
|
+
if (was === undefined) {
|
|
398
|
+
// Legacy envelope (no `was`): accept unchanged for back-compat.
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (typeof was.v === 'number' && was.v > this.#version) {
|
|
402
|
+
throw new EncryptionError(`Cannot decrypt this resource: its envelope is stamped with ` +
|
|
403
|
+
`EDV-over-WAS scheme version ${was.v}, which this client (version ` +
|
|
404
|
+
`${this.#version}) does not implement. Upgrade the client.`);
|
|
405
|
+
}
|
|
406
|
+
if (typeof was.resource === 'string') {
|
|
407
|
+
if (expectedId !== undefined && was.resource !== expectedId) {
|
|
408
|
+
throw new IntegrityError(`Cannot decrypt this resource: the stored envelope is bound to a ` +
|
|
409
|
+
`different resource id ("${was.resource}") than the one requested ` +
|
|
410
|
+
`("${expectedId}"). The server swapped two resources' envelopes.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (expectedId !== undefined) {
|
|
414
|
+
// Content-derived write (no `resource`): the id is a function of the
|
|
415
|
+
// ciphertext, so re-derive and compare.
|
|
416
|
+
const { documentCipher } = this.#edv;
|
|
417
|
+
const derived = await documentCipher.deriveId({
|
|
418
|
+
jwe: jwe
|
|
419
|
+
});
|
|
420
|
+
if (derived !== expectedId) {
|
|
421
|
+
throw new IntegrityError(`Cannot decrypt this resource: its content-derived id ("${derived}") ` +
|
|
422
|
+
`does not match the requested id ("${expectedId}"). The server ` +
|
|
423
|
+
'served this envelope under an id it was not written for.');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (this.#hasEpochs && typeof was.epoch === 'string' && keyId) {
|
|
427
|
+
const decryptedEpoch = keyId.split('#')[0];
|
|
428
|
+
if (decryptedEpoch !== was.epoch) {
|
|
429
|
+
throw new IntegrityError(`Cannot decrypt this resource: its envelope is bound to key epoch ` +
|
|
430
|
+
`"${was.epoch}" but was decrypted with a key from epoch ` +
|
|
431
|
+
`"${decryptedEpoch}". The server replayed it under a different ` +
|
|
432
|
+
'epoch.');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
321
436
|
/**
|
|
322
437
|
* @inheritdoc
|
|
323
438
|
*
|
|
@@ -327,18 +442,28 @@ export class EdvCodec {
|
|
|
327
442
|
* inert (metadata concurrency is the server's plaintext `metaVersion`, not the
|
|
328
443
|
* envelope), so each write re-encrypts fresh with no `update`.
|
|
329
444
|
*/
|
|
330
|
-
async encodeMeta({ custom }) {
|
|
331
|
-
const { documentCipher } = this
|
|
445
|
+
async encodeMeta({ custom, id: resourceId }) {
|
|
446
|
+
const { documentCipher } = this.#edv;
|
|
332
447
|
// The document needs an EDV id (the cipher asserts one on decrypt). It is
|
|
333
448
|
// opaque to the server -- carried inside the un-decryptable envelope -- and
|
|
334
449
|
// minted fresh each write, since the metadata envelope is never updated in
|
|
335
450
|
// place (concurrency is the server's plaintext `metaVersion`, Decision 3).
|
|
336
|
-
const id = (await this.
|
|
451
|
+
const id = (await this.#edv.generateId());
|
|
452
|
+
// Bind the `was` parameter to the RESOURCE id (not the metadata envelope's
|
|
453
|
+
// own random EDV id), so a server-side swap of two resources' metadata is
|
|
454
|
+
// AEAD-detected on decode. The metadata envelope always knows the resource
|
|
455
|
+
// id at encrypt time, so `resource` is always present here (never content-
|
|
456
|
+
// derived) and it carries no `epoch`.
|
|
457
|
+
const was = { v: this.#version };
|
|
458
|
+
if (resourceId !== undefined) {
|
|
459
|
+
was.resource = resourceId;
|
|
460
|
+
}
|
|
337
461
|
const encrypted = await documentCipher.encrypt({
|
|
338
462
|
doc: { id, content: custom },
|
|
339
|
-
recipients: this
|
|
340
|
-
keyResolver: this.
|
|
341
|
-
hmac: undefined
|
|
463
|
+
recipients: this.#recipients,
|
|
464
|
+
keyResolver: this.#edv.keyResolver,
|
|
465
|
+
hmac: undefined,
|
|
466
|
+
additionalProtectedParams: { was }
|
|
342
467
|
});
|
|
343
468
|
return { custom: encrypted };
|
|
344
469
|
}
|
|
@@ -350,12 +475,18 @@ export class EdvCodec {
|
|
|
350
475
|
* present value must be an EDV envelope (else {@link EncryptionError}, the
|
|
351
476
|
* `_assertEnvelope` guard), so a foreign plaintext `custom` fails closed.
|
|
352
477
|
*/
|
|
353
|
-
async decodeMeta({ custom }) {
|
|
478
|
+
async decodeMeta({ custom }, expectedId) {
|
|
354
479
|
if (custom === undefined || custom === null) {
|
|
355
480
|
return {};
|
|
356
481
|
}
|
|
357
|
-
this
|
|
358
|
-
const
|
|
482
|
+
this.#assertEnvelope(custom, 'read');
|
|
483
|
+
const encryptedDoc = custom;
|
|
484
|
+
const decrypted = await this.#decrypt(encryptedDoc);
|
|
485
|
+
await this.#verifyBinding({
|
|
486
|
+
jwe: encryptedDoc.jwe,
|
|
487
|
+
expectedId,
|
|
488
|
+
keyId: decrypted.keyId
|
|
489
|
+
});
|
|
359
490
|
return (decrypted.content ?? {});
|
|
360
491
|
}
|
|
361
492
|
/**
|
|
@@ -375,7 +506,7 @@ export class EdvCodec {
|
|
|
375
506
|
* for the message
|
|
376
507
|
* @returns {asserts doc is IEncryptedDocument}
|
|
377
508
|
*/
|
|
378
|
-
|
|
509
|
+
#assertEnvelope(doc, context) {
|
|
379
510
|
const jwe = doc !== null && typeof doc === 'object'
|
|
380
511
|
? doc.jwe
|
|
381
512
|
: undefined;
|
|
@@ -422,16 +553,16 @@ export class EdvCodec {
|
|
|
422
553
|
* @returns {Promise<{ content: Record<string, unknown>; meta:
|
|
423
554
|
* Record<string, unknown> }>}
|
|
424
555
|
*/
|
|
425
|
-
async
|
|
556
|
+
async #toDocument(data, contentType, id) {
|
|
426
557
|
const payload = resolvePayload({ data, contentType, id });
|
|
427
558
|
if (payload.kind === 'binary') {
|
|
428
559
|
const bytes = isBlob(payload.data)
|
|
429
560
|
? new Uint8Array(await payload.data.arrayBuffer())
|
|
430
561
|
: payload.data;
|
|
431
562
|
const resolvedType = payload.contentType;
|
|
432
|
-
if (bytes.length > this
|
|
563
|
+
if (bytes.length > this.#maxBlobBytes) {
|
|
433
564
|
throw new ValidationError(`Encrypted binary write of ${bytes.length} bytes exceeds the ` +
|
|
434
|
-
`single-document limit of ${this
|
|
565
|
+
`single-document limit of ${this.#maxBlobBytes} bytes. The codec ` +
|
|
435
566
|
'seam is a single-request transform and cannot chunk. To store a ' +
|
|
436
567
|
'blob this large, write it through the chunked-stream path -- ' +
|
|
437
568
|
'`EdvClientCore.insert({ doc, stream, transport })` with a ' +
|
|
@@ -483,7 +614,7 @@ export class EdvCodec {
|
|
|
483
614
|
* @param [meta] {Record<string, unknown>}
|
|
484
615
|
* @returns {Json | Blob}
|
|
485
616
|
*/
|
|
486
|
-
|
|
617
|
+
#fromDocument(content, meta) {
|
|
487
618
|
const encoding = meta?.encoding;
|
|
488
619
|
const contentType = typeof meta?.contentType === 'string' ? meta.contentType : undefined;
|
|
489
620
|
if (encoding === 'utf-8') {
|
|
@@ -522,6 +653,34 @@ function decodeUtf8(bytes) {
|
|
|
522
653
|
return null;
|
|
523
654
|
}
|
|
524
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Parses the `was` binding out of a JWE's protected header. The header is
|
|
658
|
+
* base64url (no padding) JSON; a successful decrypt has already proven it
|
|
659
|
+
* authentic, so this parse is trusted. Returns the `was` object, or `undefined`
|
|
660
|
+
* when the header is absent/unparseable or carries no `was` member (a legacy
|
|
661
|
+
* envelope that predates the binding).
|
|
662
|
+
*
|
|
663
|
+
* @param jwe {unknown}
|
|
664
|
+
* @returns {Record<string, unknown> | undefined}
|
|
665
|
+
*/
|
|
666
|
+
function parseWasHeader(jwe) {
|
|
667
|
+
const protectedHeader = jwe?.protected;
|
|
668
|
+
if (typeof protectedHeader !== 'string') {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
let parsed;
|
|
672
|
+
try {
|
|
673
|
+
parsed = JSON.parse(new TextDecoder().decode(base64urlnopad.decode(protectedHeader)));
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
const was = parsed?.was;
|
|
679
|
+
if (was === null || typeof was !== 'object') {
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
return was;
|
|
683
|
+
}
|
|
525
684
|
/**
|
|
526
685
|
* The EDV scheme tag this provider handles (matches the Collection marker).
|
|
527
686
|
*/
|
|
@@ -569,6 +728,14 @@ export function createEdvEncryption({ resolveKeys, contentType = DEFAULT_CONTENT
|
|
|
569
728
|
if (scheme !== EDV_SCHEME) {
|
|
570
729
|
return null;
|
|
571
730
|
}
|
|
731
|
+
// Refuse a marker from a future scheme version: this client does not
|
|
732
|
+
// implement it, and silently operating on it could mis-handle the data.
|
|
733
|
+
const markerVersion = encryption?.version;
|
|
734
|
+
if (typeof markerVersion === 'number' && markerVersion > 1) {
|
|
735
|
+
throw new EncryptionError(`Collection ${spaceId}/${collectionId} declares EDV-over-WAS scheme ` +
|
|
736
|
+
`version ${markerVersion}, which this client (version 1) does not ` +
|
|
737
|
+
'implement. Upgrade the client.');
|
|
738
|
+
}
|
|
572
739
|
// Prefer override-supplied keys; otherwise consult the keystore.
|
|
573
740
|
const resolved = keys ??
|
|
574
741
|
(await resolveKeys({ spaceId, collectionId }));
|
|
@@ -609,7 +776,9 @@ export function createEdvEncryption({ resolveKeys, contentType = DEFAULT_CONTENT
|
|
|
609
776
|
writeEpoch,
|
|
610
777
|
contentType,
|
|
611
778
|
maxBlobBytes,
|
|
612
|
-
idDerivation
|
|
779
|
+
idDerivation,
|
|
780
|
+
version: markerVersion ?? 1,
|
|
781
|
+
hasEpochs: !!(encryption?.epochs && encryption.epochs.length > 0)
|
|
613
782
|
});
|
|
614
783
|
}
|
|
615
784
|
};
|