openvibers/openvibe.shared ov-locker-core.js

ov-locker-core.js

main 75.8 KB · 1716 lines · javascript

1/* Generated by scripts/build-locker-core.js from openvibe-sdk 0.160.0 (src/locker). Do not edit: change the SDK,
2 * release it, bump the pin, and run the script. The SDK's files, unchanged but for the names of require and module:
3 * bytes.js 80a5a560c683f489
4 * errors.js e73f94b7711452e7
5 * format.js f7793a59fdb67ad9
6 * index.js eee174e3a87df494
7 * inspect.js 2f28ff0da8c96a4e
8 * keys.js fbe20e7949924f2b
9 * padme.js e0dd4b08e0d0f24f
10 * recovery.js 2829d07a02561674
11 * seal.js ff6ad448082c636c
12 * server.js 6177fc986b2b705d
13 * words.js 8f4f51abdadfbfd1
14 */
15(function (root) {
16 'use strict';
17 if (!root || root.OVLockerCore) return;
18 const defs = {
19 "bytes": function (__ovLockerModule, exports, __ovLockerReq) {
20'use strict';
21/**
22 * Byte helpers for the Locker envelope. Framing only, no cryptography: every value is a
23 * Uint8Array, so the same code runs in Node and in the browser.
24 */
25
26const encoder = new TextEncoder();
27const decoder = new TextDecoder();
28
29/** The UTF-8 bytes of a string. */
30function utf8(text) { return encoder.encode(text); }
31
32/** The string of UTF-8 bytes. */
33function fromUtf8(bytes) { return decoder.decode(bytes); }
34
35/** One buffer of all the parts, in order. */
36function concat(...parts) {
37 let total = 0;
38 for (const part of parts) total += part.length;
39 const out = new Uint8Array(total);
40 let at = 0;
41 for (const part of parts) { out.set(part, at); at += part.length; }
42 return out;
43}
44
45/** Standard base64. btoa/atob are in every browser and in Node. */
46function base64(bytes) {
47 let binary = '';
48 for (const byte of bytes) binary += String.fromCharCode(byte);
49 return btoa(binary);
50}
51
52/** The bytes of a standard base64 string. */
53function fromBase64(text) {
54 const binary = atob(text);
55 const out = new Uint8Array(binary.length);
56 for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
57 return out;
58}
59
60/** base64url: the same alphabet without the padding, for keys that travel in a URL. */
61function base64url(bytes) { return base64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
62
63/** The bytes of a base64url string. */
64function fromBase64url(text) {
65 const padded = text.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (text.length % 4)) % 4);
66 return fromBase64(padded);
67}
68
69/** A number as four bytes, big endian. */
70function u32be(value) {
71 const out = new Uint8Array(4);
72 new DataView(out.buffer).setUint32(0, value >>> 0, false);
73 return out;
74}
75
76/** The number four bytes at `at` hold, big endian. */
77function readU32be(bytes, at) { return new DataView(bytes.buffer, bytes.byteOffset + at, 4).getUint32(0, false); }
78
79/** A number as eight bytes, big endian. */
80function u64be(value) {
81 const out = new Uint8Array(8);
82 new DataView(out.buffer).setBigUint64(0, BigInt(value), false);
83 return out;
84}
85
86/** The number eight bytes at `at` hold, big endian. */
87function readU64be(bytes, at) { return new DataView(bytes.buffer, bytes.byteOffset + at, 8).getBigUint64(0, false); }
88
89/**
90 * The canonical JSON of a value: object keys sorted, no whitespace. Two sides that hold the
91 * same value therefore hold the same bytes, which is what a signature or an associated data
92 * needs. (JSON.stringify would keep the order the object was built in, which is not a promise
93 * we can make across two implementations.)
94 */
95function canonicalJson(value) {
96 if (value === null || typeof value !== 'object') return JSON.stringify(value);
97 if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
98 const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
99 return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
100}
101
102__ovLockerModule.exports = {
103 utf8, fromUtf8, concat, base64, fromBase64, base64url, fromBase64url,
104 u32be, readU32be, u64be, readU64be, canonicalJson,
105};
106
107 },
108 "errors": function (__ovLockerModule, exports, __ovLockerReq) {
109'use strict';
110/**
111 * The one error Locker throws. The code says what went wrong, so a caller can tell a wrong
112 * password from a truncated download from bytes that are not an envelope at all.
113 *
114 * The codes: locker.invalid (the caller asked for something impossible), locker.bad_envelope
115 * (the bytes are not an envelope we wrote), locker.truncated (the bytes stop early),
116 * locker.wrong_key (the key is not the one), locker.no_key (no key was given), locker.no_slot
117 * (there is no slot for this key), locker.size_mismatch (the size does not add up) and
118 * locker.no_crypto (the runtime has no WebCrypto).
119 */
120class LockerError extends Error {
121 constructor(code, message) {
122 super(message);
123 this.name = 'LockerError';
124 this.code = code;
125 }
126}
127
128/** A Locker error, or a plain Error with the same shape. */
129function fail(code, message) { throw new LockerError(code, message); }
130
131__ovLockerModule.exports = { LockerError, fail };
132
133 },
134 "format": function (__ovLockerModule, exports, __ovLockerReq) {
135'use strict';
136/**
137 * The Locker envelope on the wire (locker.envelope@1). This file is the format and nothing
138 * else: the numbers, the framing and the associated data. The cryptography lives in keys.js
139 * and seal.js; the rules for a service that only reads the header live in server.js.
140 *
141 * 0 4 8 8 + header length
142 * +------+------+--------------------------+--------------------------+
143 * | OVLK | len | header JSON (UTF-8) | chunk frames |
144 * +------+------+--------------------------+--------------------------+
145 *
146 * each chunk frame:
147 * 0 4 5 17
148 * +------+------+--------------------------+--------------------------+
149 * | len | flags| nonce (12 bytes) | ciphertext + 16-byte tag |
150 * +------+------+--------------------------+--------------------------+
151 */
152const { utf8, fromUtf8, concat, base64, fromBase64, u32be, readU32be, canonicalJson } = __ovLockerReq('./bytes');
153const { fail } = __ovLockerReq('./errors');
154const { padme } = __ovLockerReq('./padme');
155
156/** The four bytes that say "this is a Locker envelope". */
157const MAGIC = 'OVLK';
158/** The format this library reads and writes. */
159const VERSION = 'locker.envelope.v1';
160/** The only suite in this version: AES-256-GCM, a 32-byte key. */
161const SUITE = 'AES-256-GCM';
162/** The chunk size in bytes: 1 MiB. */
163const CHUNK_SIZE = 1048576;
164/** The GCM tag is appended to the ciphertext. */
165const TAG_SIZE = 16;
166/** The GCM nonce is 12 bytes. */
167const NONCE_SIZE = 12;
168/** The first half of every nonce: eight random bytes, in the header. */
169const NONCE_BASE_SIZE = 8;
170/** The padded plaintext starts with the true length, as eight bytes. */
171const LENGTH_PREFIX = 8;
172/**
173 * The smallest padded plaintext we write. A short note still costs 256 bytes: below that the
174 * padded length would say too much about the real one (Padmé's grid is coarse down there), and
175 * 256 bytes is nothing next to a chunk.
176 */
177const MIN_PADDED_SIZE = 256;
178/** A header is small. A bigger one is not a header we wrote, and a service must not read it. */
179const MAX_HEADER_BYTES = 65536;
180/** The bit that marks the last chunk of an envelope. */
181const FINAL_FLAG = 0x01;
182
183/**
184 * The header fields every chunk authenticates. The slots are not in here, on purpose: adding a
185 * member to a people-locked item re-wraps the content key without re-encrypting the content, so
186 * the core cannot change either. Each slot is authenticated by its own tag instead.
187 */
188function coreOf(header) {
189 return {
190 version: header.version,
191 suite: header.suite,
192 chunk_size: header.chunk_size,
193 padded_size: header.padded_size,
194 nonce_base: header.nonce_base,
195 lock: header.lock,
196 };
197}
198
199/** The canonical JSON of the core: the same bytes on both sides, whatever order the header was written in. */
200function coreBytes(header) { return utf8(canonicalJson(coreOf(header))); }
201
202/** The associated data of one chunk: the core, the chunk counter and the flags. */
203function chunkAad(header, counter, flags) {
204 return concat(coreBytes(header), u32be(counter), Uint8Array.of(flags));
205}
206
207/** The header as the bytes that go on the wire. */
208function encodeHeader(header) { return utf8(canonicalJson(header)); }
209
210/** The header's JSON bytes, as they were written. */
211function decodeHeader(headerBytes) {
212 let header;
213 try { header = JSON.parse(fromUtf8(headerBytes)); }
214 catch { fail('locker.bad_envelope', 'The envelope header is not JSON'); }
215 return header;
216}
217
218/** The whole envelope: the magic, the header and the chunks. */
219function encodeEnvelope(headerBytes, chunks) {
220 return concat(utf8(MAGIC), u32be(headerBytes.length), headerBytes, chunks);
221}
222
223/**
224 * Reads the header out of envelope bytes. It stops at the header: the chunks are not touched,
225 * so this is safe to do on bytes that are not an envelope at all.
226 */
227function readHeader(bytes) {
228 if (!bytes || bytes.length < 8) fail('locker.bad_envelope', 'The envelope is too short to hold a header');
229 const magic = fromUtf8(bytes.slice(0, 4));
230 if (magic !== MAGIC) fail('locker.bad_envelope', 'These bytes are not a Locker envelope');
231 const headerLength = readU32be(bytes, 4);
232 if (headerLength === 0 || headerLength > MAX_HEADER_BYTES) {
233 fail('locker.bad_envelope', `The envelope header is ${headerLength} bytes, which is not one we write`);
234 }
235 if (bytes.length < 8 + headerLength) fail('locker.truncated', 'The envelope ends inside its header');
236 const headerBytes = bytes.slice(8, 8 + headerLength);
237 return { header: decodeHeader(headerBytes), headerBytes, chunkOffset: 8 + headerLength };
238}
239
240/**
241 * How many chunks a padded plaintext of this size takes. Every chunk but the last is a whole
242 * chunk; the last holds what is left of it.
243 */
244function chunkCount(padded) { return Math.max(1, Math.ceil(padded / CHUNK_SIZE)); }
245
246/** How many bytes the last chunk of a padded plaintext of this size holds: more than zero. */
247function lastChunkSize(padded) { return padded - (chunkCount(padded) - 1) * CHUNK_SIZE; }
248
249/**
250 * The padded size of a content of this size: the length prefix plus the content, padded with
251 * Padmé up to the next size on its grid, and never below the floor.
252 */
253function paddedSize(size) { return Math.max(MIN_PADDED_SIZE, padme(LENGTH_PREFIX + size)); }
254
255/** The nonce of a chunk: the header's base, then the counter. No nonce repeats inside an envelope. */
256function chunkNonce(nonceBase, counter) {
257 const nonce = new Uint8Array(NONCE_SIZE);
258 nonce.set(nonceBase, 0);
259 new DataView(nonce.buffer).setUint32(8, counter >>> 0, false);
260 return nonce;
261}
262
263/** One chunk frame: its length, its flags, its nonce and the ciphertext with the tag. */
264function encodeChunk(counter, flags, nonce, ciphertext) {
265 return concat(u32be(ciphertext.length), Uint8Array.of(flags), nonce, ciphertext);
266}
267
268/** The header's nonce base as bytes. */
269function nonceBaseBytes(header) {
270 const bytes = fromBase64(header.nonce_base);
271 if (bytes.length !== NONCE_BASE_SIZE) fail('locker.bad_envelope', 'The envelope nonce base is not eight bytes');
272 return bytes;
273}
274
275__ovLockerModule.exports = {
276 MAGIC, VERSION, SUITE, CHUNK_SIZE, TAG_SIZE, NONCE_SIZE, NONCE_BASE_SIZE, LENGTH_PREFIX,
277 MIN_PADDED_SIZE, MAX_HEADER_BYTES, FINAL_FLAG,
278 coreOf, coreBytes, chunkAad, encodeHeader, decodeHeader, encodeEnvelope, readHeader,
279 chunkCount, lastChunkSize, paddedSize, chunkNonce, encodeChunk, nonceBaseBytes,
280};
281
282 },
283 "index": function (__ovLockerModule, exports, __ovLockerReq) {
284'use strict';
285/**
286 * openvibe-sdk/locker: end-to-end encryption for anything on OpenVibe.
287 *
288 * One format, one library, two runtimes: Node and the browser, WebCrypto only. A locked item is
289 * stored and served as ciphertext; the keys never reach our servers.
290 *
291 * const { seal, open, generatePersonKeys } = locker; // openvibe-sdk/locker
292 * const person = await generatePersonKeys();
293 * const sealed = await seal('a note', { lock: 'people', keys: [{ subject, publicKey: person.ecdhPublic }] });
294 * const note = await open(sealed, { privateKey: person.ecdh.privateKey, subject });
295 */
296const format = __ovLockerReq('./format');
297const keys = __ovLockerReq('./keys');
298const seal = __ovLockerReq('./seal');
299const { padme } = __ovLockerReq('./padme');
300const { inspect } = __ovLockerReq('./inspect');
301const { recoveryWords, isRecoveryPhrase, RECOVERY_WORDS } = __ovLockerReq('./recovery');
302const { WORDS } = __ovLockerReq('./words');
303const { LockerError } = __ovLockerReq('./errors');
304
305__ovLockerModule.exports = {
306 // The format.
307 VERSION: format.VERSION,
308 SUITE: format.SUITE,
309 CHUNK_SIZE: format.CHUNK_SIZE,
310 MIN_PADDED_SIZE: format.MIN_PADDED_SIZE,
311 MAX_HEADER_BYTES: format.MAX_HEADER_BYTES,
312 PASSWORD_ITERATIONS: keys.PASSWORD_ITERATIONS,
313 PASSWORD_ITERATIONS_MIN: keys.PASSWORD_ITERATIONS_MIN,
314 PASSWORD_ITERATIONS_MAX: keys.PASSWORD_ITERATIONS_MAX,
315
316 // The content.
317 seal: seal.seal,
318 open: seal.open,
319 sealStream: seal.sealStream,
320 openStream: seal.openStream,
321 rewrap: seal.rewrap,
322
323 // What an envelope says about itself, without a key.
324 inspect,
325
326 // The padding: the padded size of a content of a given size.
327 padme,
328
329 // The recovery phrase: words to write down, and a check on a typed one.
330 recoveryWords,
331 isRecoveryPhrase,
332 RECOVERY_WORDS,
333 WORDS,
334
335 // A person's keys, and the eight words that say which keys they are.
336 generatePersonKeys: keys.generatePersonKeys,
337 exportPersonKeys: keys.exportPersonKeys,
338 exportPrivateKeys: keys.exportPrivateKeys,
339 importPersonKeys: keys.importPersonKeys,
340 verifyPersonKeys: keys.verifyPersonKeys,
341 fingerprintOf: keys.fingerprintOf,
342 fingerprintWords: keys.fingerprintWords,
343 jwkThumbprint: keys.jwkThumbprint,
344
345 // The key slots: the content key, wrapped for one person or for one password.
346 wrapFor: keys.wrapFor,
347 wrapForPassword: keys.wrapForPassword,
348 unwrapPeopleSlot: keys.unwrapPeopleSlot,
349 unwrapPasswordSlot: keys.unwrapPasswordSlot,
350
351 LockerError,
352};
353
354 },
355 "inspect": function (__ovLockerModule, exports, __ovLockerReq) {
356'use strict';
357/**
358 * inspect: what an envelope says about itself, without a key and without decrypting anything.
359 *
360 * A service, or a page that shows a locked item, often needs to know the lock kind, how big
361 * the item is, who it is for and how strong the password is — before it has any key at all.
362 * This reads the header and nothing else: no chunk is touched and no key is used.
363 *
364 * The header holds no content, only wrapped keys, so this says nothing the server does not
365 * already see.
366 */
367const { envelopeBytes, readCore } = __ovLockerReq('./seal');
368const { readHeader } = __ovLockerReq('./format');
369
370/**
371 * What an envelope says about itself.
372 *
373 * { version, lock, padded_size, recipients, iterations }
374 *
375 * `recipients` holds the thumbprint of each recipient's key for a people lock (the same
376 * strings the slots carry), and `iterations` the PBKDF2 count for a password lock. The other
377 * field is empty for the other lock kinds.
378 */
379function inspect(bytes) {
380 const data = envelopeBytes(bytes);
381 const { header } = readHeader(data);
382 readCore(header);
383 const out = {
384 version: header.version,
385 lock: header.lock,
386 padded_size: header.padded_size,
387 recipients: [],
388 iterations: null,
389 };
390 if (header.lock === 'people') {
391 out.recipients = (header.slots.people || []).map(slot => slot.recipient_key);
392 } else if (header.lock === 'password' && header.slots.password) {
393 out.iterations = header.slots.password.iterations;
394 }
395 return out;
396}
397
398__ovLockerModule.exports = { inspect };
399
400 },
401 "keys": function (__ovLockerModule, exports, __ovLockerReq) {
402'use strict';
403/**
404 * The keys: a person's two key pairs, the content key, and the two ways of wrapping the content
405 * key for someone else (their public key, or a password).
406 *
407 * WebCrypto only: globalThis.crypto.subtle. No other crypto library, nothing hand-rolled: the
408 * ECDH, the ECDSA, the HKDF, the PBKDF2 and the AES-GCM are all the browser's own.
409 */
410const { utf8, concat, base64, base64url, fromBase64, canonicalJson } = __ovLockerReq('./bytes');
411const { fail } = __ovLockerReq('./errors');
412const { WORDS } = __ovLockerReq('./words');
413
414/** The suite of the key pairs: P-256, which every browser's WebCrypto has. */
415const CURVE = 'P-256';
416/** How the content key is wrapped for a person. */
417const PEOPLE_ALGORITHM = 'ECDH-P256/HKDF-SHA256/AES-256-GCM';
418/** How the content key is wrapped for a password. */
419const PASSWORD_ALGORITHM = 'PBKDF2-SHA256/AES-256-GCM';
420/** The PBKDF2 floor for this version. The count may go up; it may not go down. */
421const PASSWORD_ITERATIONS = 600000;
422/** The smallest PBKDF2 count we accept, on both sides: the floor above. */
423const PASSWORD_ITERATIONS_MIN = 600000;
424/**
425 * The largest PBKDF2 count we accept. The count comes from the header, so a hostile header could
426 * ask for a derivation that takes hours; the ceiling keeps a browser honest.
427 */
428const PASSWORD_ITERATIONS_MAX = 10000000;
429/** The content key is 32 bytes: AES-256. */
430const KEY_BYTES = 32;
431/** The salt of a people slot (HKDF) and of a password slot (PBKDF2). */
432const SALT_BYTES = 16;
433
434/** WebCrypto, or a clear error when the runtime has none. */
435function subtle() {
436 const crypto = globalThis.crypto;
437 if (!crypto || !crypto.subtle) {
438 fail('locker.no_crypto', 'This runtime has no WebCrypto (globalThis.crypto.subtle)');
439 }
440 return crypto.subtle;
441}
442
443/** Random bytes from the platform. */
444function platformRandom(length) {
445 const bytes = new Uint8Array(length);
446 globalThis.crypto.getRandomValues(bytes);
447 return bytes;
448}
449
450/** The SHA-256 of some bytes. */
451async function sha256(bytes) {
452 return new Uint8Array(await subtle().digest('SHA-256', bytes));
453}
454
455/** The bytes of a public JWK, as they go on the wire: no key_ops, no ext. */
456function publicJwk(jwk) {
457 return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
458}
459
460/** The message a person signs: both public keys, canonically. */
461function fingerprintMessage(ecdhPublic, signingPublic) {
462 return utf8(canonicalJson({ ecdh: publicJwk(ecdhPublic), signing: publicJwk(signingPublic) }));
463}
464
465/**
466 * The eight words of a fingerprint: the first eight bytes of SHA-256, eight bits each, in two
467 * groups of four. Sixty-four bits, not thirty-two: a server that swapped in its own key would
468 * need about 2^32 key generations to land on the same four words, which is not "verify a
469 * person"; with eight words it would need 2^64.
470 *
471 * The two groups are joined with a middle dot, not a hyphen: a hyphen reads as one compound
472 * word ("plateau yew-apple petal"), and the groups are meant to be read apart.
473 */
474function fingerprintWords(hash) {
475 const words = [];
476 for (let i = 0; i < 8; i++) words.push(WORDS[hash[i]]);
477 return `${words.slice(0, 4).join(' ')} · ${words.slice(4).join(' ')}`;
478}
479
480/**
481 * The RFC 7638 thumbprint of a public JWK: SHA-256 over the canonical JSON of its four
482 * required members, base64url. It names one key and nothing else, so a slot can be bound to
483 * the key of the person it is for.
484 */
485async function jwkThumbprint(jwk) {
486 const canonical = canonicalJson({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
487 return base64url(await sha256(utf8(canonical)));
488}
489
490/** The fingerprint of a pair of public keys. */
491async function fingerprintOf(ecdhPublic, signingPublic) {
492 return fingerprintWords(await sha256(fingerprintMessage(ecdhPublic, signingPublic)));
493}
494
495/**
496 * A person's two key pairs: an ECDH pair that receives content keys, and an ECDSA pair that signs
497 * the fingerprint. The signing key signs both public keys, so the pair is self-certifying: a
498 * reader can check that the holder owns the signing key, and the fingerprint changes whenever
499 * either key changes.
500 */
501async function generatePersonKeys() {
502 const s = subtle();
503 const ecdh = await s.generateKey({ name: 'ECDH', namedCurve: CURVE }, true, ['deriveBits']);
504 const signing = await s.generateKey({ name: 'ECDSA', namedCurve: CURVE }, true, ['sign', 'verify']);
505 const ecdhPublic = publicJwk(await s.exportKey('jwk', ecdh.publicKey));
506 const signingPublic = publicJwk(await s.exportKey('jwk', signing.publicKey));
507 const message = fingerprintMessage(ecdhPublic, signingPublic);
508 const signature = new Uint8Array(await s.sign({ name: 'ECDSA', hash: 'SHA-256' }, signing.privateKey, message));
509 return {
510 ecdh: { publicKey: ecdh.publicKey, privateKey: ecdh.privateKey },
511 signing: { publicKey: signing.publicKey, privateKey: signing.privateKey },
512 ecdhPublic,
513 signingPublic,
514 fingerprint: fingerprintWords(await sha256(message)),
515 fingerprintSignature: signature,
516 };
517}
518
519/** The public half of a person's keys, as it is published: locker.public-keys@1. */
520async function exportPersonKeys(person) {
521 const ecdhPublic = publicJwk(person.ecdhPublic || await subtle().exportKey('jwk', person.ecdh.publicKey));
522 const signingPublic = publicJwk(person.signingPublic || await subtle().exportKey('jwk', person.signing.publicKey));
523 const message = fingerprintMessage(ecdhPublic, signingPublic);
524 const signature = person.fingerprintSignature
525 || new Uint8Array(await subtle().sign({ name: 'ECDSA', hash: 'SHA-256' }, (person.signing?.privateKey || person.signingPrivateKey), message));
526 return {
527 ecdh: ecdhPublic,
528 signing: signingPublic,
529 fingerprint: fingerprintWords(await sha256(message)),
530 fingerprint_signature: base64(signature),
531 };
532}
533
534/**
535 * The private half of a person's keys, as it is stored: the two private keys as JWKs.
536 */
537async function exportPrivateKeys(person) {
538 const ecdh = await subtle().exportKey('jwk', person.ecdh?.privateKey || person.ecdhPrivateKey);
539 const signing = await subtle().exportKey('jwk', person.signing?.privateKey || person.signingPrivateKey);
540 return { ecdh, signing };
541}
542
543/**
544 * A person's keys, ready to use: the private keys as CryptoKeys and the public keys as JWKs.
545 * The private keys may be JWKs (as they are stored) or CryptoKeys (as they are in memory).
546 */
547async function importPersonKeys(data) {
548 const s = subtle();
549 const ecdhPrivate = data.ecdhPrivateKey
550 || await s.importKey('jwk', data.ecdh, { name: 'ECDH', namedCurve: CURVE }, true, ['deriveBits']);
551 const signingPrivate = data.signingPrivateKey
552 || await s.importKey('jwk', data.signing, { name: 'ECDSA', namedCurve: CURVE }, true, ['sign']);
553 return {
554 ecdhPrivateKey: ecdhPrivate,
555 signingPrivateKey: signingPrivate,
556 ecdhPublic: data.ecdh ? publicJwk(data.ecdh) : publicJwk(await s.exportKey('jwk', ecdhPrivate)),
557 signingPublic: data.signing ? publicJwk(data.signing) : publicJwk(await s.exportKey('jwk', signingPrivate)),
558 };
559}
560
561/**
562 * Checks a published set of keys: the signature must verify with the signing key, and the four
563 * words must be the fingerprint of the two keys. Returns false instead of throwing, so a caller
564 * can show a warning rather than an error.
565 */
566async function verifyPersonKeys(data) {
567 try {
568 const s = subtle();
569 const message = fingerprintMessage(data.ecdh, data.signing);
570 const signature = fromBase64(data.fingerprint_signature);
571 const key = await s.importKey('jwk', data.signing, { name: 'ECDSA', namedCurve: CURVE }, true, ['verify']);
572 const ok = await s.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, signature, message);
573 if (!ok) return false;
574 return fingerprintWords(await sha256(message)) === data.fingerprint;
575 } catch {
576 return false;
577 }
578}
579
580/** The content key as an AES-GCM CryptoKey. */
581async function importContentKey(bytes, usages) {
582 if (!bytes || bytes.length !== KEY_BYTES) fail('locker.invalid', 'The content key is 32 bytes');
583 return subtle().importKey('raw', bytes, { name: 'AES-GCM', length: 256 }, false, usages);
584}
585
586/** The wrapping key for a person: the ECDH secret, put through HKDF-SHA-256. */
587async function deriveWrappingKey(sharedSecret, salt, info) {
588 const s = subtle();
589 const material = await s.importKey('raw', sharedSecret, 'HKDF', false, ['deriveKey']);
590 return s.deriveKey(
591 { name: 'HKDF', hash: 'SHA-256', salt, info },
592 material,
593 { name: 'AES-GCM', length: 256 },
594 false,
595 ['encrypt', 'decrypt'],
596 );
597}
598
599/** The wrapping key for a password: PBKDF2-SHA-256, with the parameters the header names. */
600async function derivePasswordKey(password, salt, iterations) {
601 if (typeof password !== 'string' || !password.length) fail('locker.invalid', 'A password lock needs a password');
602 // The count comes from the header, so it is checked before it is used: a hostile header must
603 // not be able to make a reader spend minutes (or hours) on the derivation.
604 if (!Number.isInteger(iterations) || iterations < PASSWORD_ITERATIONS_MIN || iterations > PASSWORD_ITERATIONS_MAX) {
605 fail('locker.bad_envelope', `The iteration count is not between ${PASSWORD_ITERATIONS_MIN} and ${PASSWORD_ITERATIONS_MAX}`);
606 }
607 const s = subtle();
608 const material = await s.importKey('raw', utf8(password), 'PBKDF2', false, ['deriveKey']);
609 return s.deriveKey(
610 { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
611 material,
612 { name: 'AES-GCM', length: 256 },
613 false,
614 ['encrypt', 'decrypt'],
615 );
616}
617
618/**
619 * The associated data of a people slot: it binds the slot to one person, to that person's key
620 * and to one envelope.
621 */
622function peopleSlotAad(subject, ephemeralPublic, salt, recipientKey) {
623 const id = typeof subject === 'string' ? subject : `${subject.type}:${subject.id}`;
624 return utf8(`openvibe-locker people-slot v1|${id}|${recipientKey}|${canonicalJson(publicJwk(ephemeralPublic))}|${base64(salt)}`);
625}
626
627/** The associated data of a password slot: it binds the slot to its own parameters. */
628function passwordSlotAad(iterations, salt) {
629 return utf8(`openvibe-locker password-slot v1|${iterations}|${base64(salt)}`);
630}
631
632/** A public key as WebCrypto wants it. */
633async function importPublicKey(jwk) {
634 try {
635 return await subtle().importKey('jwk', publicJwk(jwk), { name: 'ECDH', namedCurve: CURVE }, true, []);
636 } catch {
637 fail('locker.invalid', 'The public key is not a P-256 key');
638 }
639}
640
641/**
642 * The envelope's one ephemeral ECDH key pair. It may be given (the shared test vectors need the
643 * same bytes every time), or made fresh.
644 */
645async function resolveEphemeral(ephemeral) {
646 const s = subtle();
647 if (!ephemeral) {
648 const pair = await s.generateKey({ name: 'ECDH', namedCurve: CURVE }, true, ['deriveBits']);
649 return { privateKey: pair.privateKey, publicKey: pair.publicKey };
650 }
651 if (ephemeral.privateKey) return ephemeral;
652 const privateKey = await s.importKey('jwk', ephemeral.private, { name: 'ECDH', namedCurve: CURVE }, true, ['deriveBits']);
653 return { privateKey };
654}
655
656/**
657 * Wraps the content key for one person: the content key, sealed for their public key.
658 * The envelope has one ephemeral ECDH key pair; the shared secret with each person goes through
659 * HKDF-SHA-256, and the slot's tag covers the person, their key, the ephemeral key and the
660 * salt, so a slot cannot be moved to another person or replayed from another envelope.
661 */
662async function wrapFor(contentKey, publicKey, { subject, salt, random = platformRandom, ephemeral } = {}) {
663 const s = subtle();
664 if (!subject) fail('locker.invalid', 'A people slot needs the subject it is for');
665 const theirs = await importPublicKey(publicKey);
666 const pair = await resolveEphemeral(ephemeral);
667 const ephemeralPublic = publicJwk(await s.exportKey('jwk', pair.publicKey || pair.privateKey));
668 // The slot names the key it is for, so an opener can check it before deriving anything.
669 const recipientKey = await jwkThumbprint(publicJwk(publicKey));
670 const shared = new Uint8Array(await s.deriveBits({ name: 'ECDH', public: theirs }, pair.privateKey, 256));
671 const slotSalt = salt || random(SALT_BYTES);
672 const key = await deriveWrappingKey(shared, slotSalt, utf8(`openvibe-locker people-slot v1|${subject.type}:${subject.id}|${recipientKey}`));
673 const nonce = random(12);
674 const sealed = new Uint8Array(await s.encrypt(
675 { name: 'AES-GCM', iv: nonce, additionalData: peopleSlotAad(subject, ephemeralPublic, slotSalt, recipientKey) },
676 key,
677 contentKey,
678 ));
679 return {
680 subject,
681 algorithm: PEOPLE_ALGORITHM,
682 recipient_key: recipientKey,
683 ephemeral_public_key: ephemeralPublic,
684 salt: base64(slotSalt),
685 nonce: base64(nonce),
686 wrapped_key: base64(sealed),
687 };
688}
689
690/** The content key out of a people slot: the recipient's private key and the slot. */
691async function unwrapPeopleSlot(slot, privateKey) {
692 const s = subtle();
693 // The slot names the key it was made for. If it is not this key, the slot is not this
694 // person's, and nothing is derived: the check comes before the work.
695 if (!slot.recipient_key) fail('locker.bad_envelope', 'The slot does not name the key it is for');
696 const theirPublic = publicJwk(await s.exportKey('jwk', privateKey));
697 const recipientKey = await jwkThumbprint(theirPublic);
698 if (slot.recipient_key !== recipientKey) {
699 fail('locker.wrong_key', 'This slot is for another key');
700 }
701 const theirs = await s.importKey('jwk', publicJwk(slot.ephemeral_public_key), { name: 'ECDH', namedCurve: CURVE }, true, []);
702 const shared = new Uint8Array(await s.deriveBits({ name: 'ECDH', public: theirs }, privateKey, 256));
703 const salt = fromBase64(slot.salt);
704 const key = await deriveWrappingKey(shared, salt, utf8(`openvibe-locker people-slot v1|${slot.subject.type}:${slot.subject.id}|${recipientKey}`));
705 try {
706 const plain = new Uint8Array(await s.decrypt(
707 { name: 'AES-GCM', iv: fromBase64(slot.nonce), additionalData: peopleSlotAad(slot.subject, slot.ephemeral_public_key, salt, recipientKey) },
708 key,
709 fromBase64(slot.wrapped_key),
710 ));
711 if (plain.length !== KEY_BYTES) fail('locker.wrong_key', 'The slot does not hold a content key');
712 return plain;
713 } catch (err) {
714 if (err && err.code) throw err;
715 fail('locker.wrong_key', 'This key does not open that slot');
716 }
717}
718
719/** Wraps the content key with a password: PBKDF2-SHA-256, then AES-256-GCM. */
720async function wrapForPassword(contentKey, password, { salt, iterations = PASSWORD_ITERATIONS, random = platformRandom } = {}) {
721 const s = subtle();
722 const slotSalt = salt || random(SALT_BYTES);
723 const key = await derivePasswordKey(password, slotSalt, iterations);
724 const nonce = random(12);
725 const sealed = new Uint8Array(await s.encrypt(
726 { name: 'AES-GCM', iv: nonce, additionalData: passwordSlotAad(iterations, slotSalt) },
727 key,
728 contentKey,
729 ));
730 return {
731 algorithm: PASSWORD_ALGORITHM,
732 kdf: 'PBKDF2-SHA-256',
733 iterations,
734 salt: base64(slotSalt),
735 nonce: base64(nonce),
736 wrapped_key: base64(sealed),
737 };
738}
739
740/** The content key out of a password slot. The parameters come from the slot, never from here. */
741async function unwrapPasswordSlot(slot, password) {
742 const s = subtle();
743 const salt = fromBase64(slot.salt);
744 const key = await derivePasswordKey(password, salt, slot.iterations);
745 try {
746 const plain = new Uint8Array(await s.decrypt(
747 { name: 'AES-GCM', iv: fromBase64(slot.nonce), additionalData: passwordSlotAad(slot.iterations, salt) },
748 key,
749 fromBase64(slot.wrapped_key),
750 ));
751 if (plain.length !== KEY_BYTES) fail('locker.wrong_key', 'The slot does not hold a content key');
752 return plain;
753 } catch (err) {
754 if (err && err.code) throw err;
755 fail('locker.wrong_key', 'That password does not open this item');
756 }
757}
758
759__ovLockerModule.exports = {
760 CURVE, PEOPLE_ALGORITHM, PASSWORD_ALGORITHM, PASSWORD_ITERATIONS,
761 PASSWORD_ITERATIONS_MIN, PASSWORD_ITERATIONS_MAX, KEY_BYTES, SALT_BYTES,
762 subtle, platformRandom, sha256, publicJwk, fingerprintMessage, fingerprintWords, fingerprintOf,
763 jwkThumbprint, generatePersonKeys, exportPersonKeys, exportPrivateKeys, importPersonKeys,
764 verifyPersonKeys, importContentKey, deriveWrappingKey, derivePasswordKey, resolveEphemeral,
765 peopleSlotAad, passwordSlotAad, wrapFor, unwrapPeopleSlot, wrapForPassword, unwrapPasswordSlot,
766};
767
768 },
769 "padme": function (__ovLockerModule, exports, __ovLockerReq) {
770'use strict';
771/**
772 * Padmé padding (Nikitin et al., "Reducing Metadata Leakage from Encrypted Files and Network
773 * Traffic", PoPETs 2019).
774 *
775 * The point of padding a locked item: the server must not learn how big it is. Rounding the size
776 * up to a whole 1 MiB chunk says too much — a 200-byte note becomes a 1 MiB object, and every
777 * item under a megabyte looks the same, which is a leak of its own.
778 *
779 * Padmé rounds the length up to the next size on a grid that gets finer as the length grows:
780 *
781 * E = floor(log2(n)) the band the length is in
782 * S = floor(log2(E)) + 1 how many steps the band has
783 * d = 2^(E - S) the step of the band
784 * P(n) = d * ceil(n / d) the padded length
785 *
786 * The padded length is one of a small number of values per band, and the bands double, so the
787 * number of possible padded lengths up to n grows like log(n): knowing the padded length says
788 * O(log log n) bits about the real one. The expansion stays under 12% (the worst case is a
789 * length just above a band boundary, 129 bytes padded to 144).
790 *
791 * The arithmetic is exact above 2^31: there are no 32-bit bitwise operators here, only
792 * comparisons and division by a power of two, both exact on the doubles we have.
793 */
794
795/** The largest e with 2^e <= size: floor(log2(size)), without a logarithm. */
796function exponentOf(size) {
797 let e = 0;
798 let power = 1;
799 while (power * 2 <= size) { power *= 2; e++; }
800 return e;
801}
802
803/**
804 * The padded length of a content of this size: the smallest length on Padmé's grid that is not
805 * smaller than it. Zero for zero, which the caller handles.
806 */
807function padme(size) {
808 if (!Number.isInteger(size) || size <= 0) return 0;
809 if (size === 1) return 1;
810 const band = exponentOf(size);
811 const steps = exponentOf(band) + 1;
812 const step = Math.pow(2, band - steps);
813 // Dividing by a power of two is exact, so the ceiling is the ceiling and not nearly it.
814 return Math.ceil(size / step) * step;
815}
816
817__ovLockerModule.exports = { padme, exponentOf };
818
819 },
820 "recovery": function (__ovLockerModule, exports, __ovLockerReq) {
821'use strict';
822/**
823 * The recovery phrase: the words a person writes down.
824 *
825 * A phrase of n words from the same 256-word list the fingerprint uses, one byte per word from
826 * the platform's CSPRNG (crypto.getRandomValues). Eight bits per word, so the usual 24 words
827 * carry 192 bits: enough for a key nobody else has, and short enough to write on paper.
828 *
829 * There is no checksum in the phrase: the words are the secret, and a phrase that is typed
830 * wrong simply does not open anything. isRecoveryPhrase only says whether every word is one of
831 * the 256, so a UI can tell a typo from a word that was never in the list.
832 */
833const { fail } = __ovLockerReq('./errors');
834const { platformRandom } = __ovLockerReq('./keys');
835const { WORDS } = __ovLockerReq('./words');
836
837/** How many words a recovery phrase has, unless the caller says otherwise. */
838const RECOVERY_WORDS = 24;
839
840/**
841 * A recovery phrase: n words from the list, one byte each.
842 *
843 * recoveryWords() // 24 words, 192 bits
844 * recoveryWords(12) // 12 words, 96 bits
845 */
846function recoveryWords(count = RECOVERY_WORDS, { random = platformRandom } = {}) {
847 if (!Number.isInteger(count) || count < 1 || count > WORDS.length) {
848 fail('locker.invalid', `A recovery phrase is one to ${WORDS.length} words`);
849 }
850 // One byte per word: the list has 256 words, so a byte picks one of them exactly.
851 const bytes = random(count);
852 const words = [];
853 for (const byte of bytes) words.push(WORDS[byte]);
854 return words.join(' ');
855}
856
857/**
858 * Whether a typed phrase is made of words from the list. It says nothing about the order or
859 * about whether the phrase is the right one: only that every word could have come from us.
860 */
861function isRecoveryPhrase(phrase) {
862 if (typeof phrase !== 'string') return false;
863 const words = phrase.trim().split(/\s+/).filter(Boolean);
864 if (!words.length) return false;
865 return words.every(word => WORDS.includes(word.toLowerCase()));
866}
867
868__ovLockerModule.exports = { RECOVERY_WORDS, recoveryWords, isRecoveryPhrase };
869
870 },
871 "seal": function (__ovLockerModule, exports, __ovLockerReq) {
872'use strict';
873/**
874 * seal and open: the content, the three locks, and the streams.
875 *
876 * The envelope is written once and read once. Every chunk is sealed with AES-256-GCM under the
877 * content key, with the header's core, the chunk counter and the flags as associated data, so a
878 * chunk cannot be cut, reordered or truncated without the open failing.
879 */
880const {
881 utf8, fromUtf8, concat, base64, base64url, fromBase64url, u32be, readU32be, u64be, readU64be,
882} = __ovLockerReq('./bytes');
883const { fail } = __ovLockerReq('./errors');
884const {
885 MAGIC, VERSION, SUITE, CHUNK_SIZE, TAG_SIZE, NONCE_BASE_SIZE, LENGTH_PREFIX, MIN_PADDED_SIZE,
886 FINAL_FLAG, coreOf, chunkAad, encodeHeader, encodeEnvelope, readHeader, chunkCount,
887 lastChunkSize, paddedSize, chunkNonce, encodeChunk, nonceBaseBytes,
888} = __ovLockerReq('./format');
889const {
890 KEY_BYTES, PASSWORD_ITERATIONS, subtle, platformRandom, importContentKey, publicJwk,
891 jwkThumbprint, wrapFor, unwrapPeopleSlot, wrapForPassword, unwrapPasswordSlot, resolveEphemeral,
892} = __ovLockerReq('./keys');
893
894/** The content as bytes: a string is UTF-8, anything else is taken as it is. */
895function toBytes(content) {
896 if (typeof content === 'string') return utf8(content);
897 if (content instanceof Uint8Array) return content;
898 if (content instanceof ArrayBuffer) return new Uint8Array(content);
899 if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
900 fail('locker.invalid', 'The content must be a string or bytes');
901}
902
903/** The bytes of an envelope: what seal returns, or what the caller kept. */
904function envelopeBytes(envelope) {
905 if (envelope instanceof Uint8Array) return envelope;
906 if (envelope && envelope.bytes instanceof Uint8Array) return envelope.bytes;
907 fail('locker.invalid', 'That is not a Locker envelope');
908}
909
910/** A source of bytes: an async iterable of Uint8Array. */
911async function* once(bytes) { yield bytes; }
912
913/**
914 * Collects bytes into blocks of a fixed size, without ever holding more than one block and the
915 * part that has not been used yet. This is what keeps a stream seal in constant memory.
916 */
917class Chunker {
918 constructor(size) { this.size = size; this.parts = []; this.length = 0; }
919
920 push(part) { this.parts.push(part); this.length += part.length; }
921
922 /** The next full block, or null when there is not enough yet. */
923 take() {
924 if (this.length < this.size) return null;
925 const block = new Uint8Array(this.size);
926 let at = 0;
927 while (at < this.size) {
928 const head = this.parts[0];
929 const take = Math.min(head.length, this.size - at);
930 block.set(head.subarray(0, take), at);
931 at += take;
932 if (take === head.length) this.parts.shift();
933 else this.parts[0] = head.subarray(take);
934 }
935 this.length -= this.size;
936 return block;
937 }
938
939 /** What is left, as one block of exactly this many bytes: the bytes that are there, then zeros. */
940 takeExactly(count) {
941 if (this.length > count) fail('locker.size_mismatch', 'The content is longer than the size it was given');
942 const block = new Uint8Array(count);
943 let at = 0;
944 for (const part of this.parts) { block.set(part, at); at += part.length; }
945 this.parts = [];
946 this.length = 0;
947 return block;
948 }
949}
950
951/** The header, the content key and the nonce base: everything a seal needs before it writes. */
952async function prepare(options) {
953 const lock = options.lock;
954 if (!['people', 'password', 'link'].includes(lock)) {
955 fail('locker.invalid', 'A lock is people, password or link');
956 }
957 const random = options.random || platformRandom;
958 const contentKey = options.contentKey ? toBytes(options.contentKey) : random(KEY_BYTES);
959 if (contentKey.length !== KEY_BYTES) fail('locker.invalid', 'The content key is 32 bytes');
960 const nonceBase = options.nonceBase ? toBytes(options.nonceBase) : random(NONCE_BASE_SIZE);
961 if (nonceBase.length !== NONCE_BASE_SIZE) fail('locker.invalid', 'The nonce base is eight bytes');
962 let slots;
963 if (lock === 'people') {
964 const keys = options.keys || [];
965 if (!keys.length) fail('locker.invalid', 'A people lock needs at least one recipient');
966 // One ephemeral key pair for the whole envelope: the shared secret differs per person.
967 const pair = await resolveEphemeral(options.ephemeral);
968 const people = [];
969 for (const key of keys) {
970 people.push(await wrapFor(contentKey, key.publicKey, {
971 subject: key.subject, random, ephemeral: pair,
972 }));
973 }
974 slots = { people };
975 } else if (lock === 'password') {
976 slots = {
977 password: await wrapForPassword(contentKey, options.password, {
978 salt: options.salt, iterations: options.iterations || PASSWORD_ITERATIONS, random,
979 }),
980 };
981 } else {
982 // A link lock stores no key: the content key travels in the part of the link after the #.
983 slots = { link: {} };
984 }
985 return { contentKey, nonceBase, slots };
986}
987
988/** The header of an envelope. */
989function makeHeader(lock, slots, padded, nonceBase) {
990 return {
991 version: VERSION,
992 suite: SUITE,
993 chunk_size: CHUNK_SIZE,
994 padded_size: padded,
995 nonce_base: base64(nonceBase),
996 lock,
997 slots,
998 };
999}
1000
1001/** Seals one block of the padded plaintext. */
1002async function sealBlock(key, header, nonceBase, counter, block, isFinal) {
1003 const flags = isFinal ? FINAL_FLAG : 0;
1004 const nonce = chunkNonce(nonceBase, counter);
1005 const ciphertext = new Uint8Array(await subtle().encrypt(
1006 { name: 'AES-GCM', iv: nonce, additionalData: chunkAad(header, counter, flags) },
1007 key,
1008 block,
1009 ));
1010 return encodeChunk(counter, flags, nonce, ciphertext);
1011}
1012
1013/**
1014 * The envelope, piece by piece. The header comes first (it holds the padded size, so the size
1015 * has to be known before the first byte is written), then one frame per chunk.
1016 */
1017async function* sealPieces(source, options) {
1018 const size = options.size;
1019 const { contentKey, nonceBase, slots } = await prepare(options);
1020 const padded = paddedSize(size);
1021 const header = makeHeader(options.lock, slots, padded, nonceBase);
1022 const headerBytes = encodeHeader(header);
1023 yield { header, headerBytes, contentKey, bytes: concat(utf8(MAGIC), u32be(headerBytes.length), headerBytes) };
1024 const key = await importContentKey(contentKey, ['encrypt']);
1025 const total = chunkCount(padded);
1026 const chunker = new Chunker(CHUNK_SIZE);
1027 let counter = 0;
1028 // The padded plaintext starts with the true length, so the server cannot read the size.
1029 chunker.push(u64be(size));
1030 for await (const part of source) {
1031 chunker.push(part);
1032 // Every chunk but the last is a whole chunk.
1033 for (;;) {
1034 if (counter >= total - 1) break;
1035 const block = chunker.take();
1036 if (!block) break;
1037 yield { bytes: await sealBlock(key, header, nonceBase, counter, block, false) };
1038 counter++;
1039 }
1040 }
1041 // The content is done; the padding fills the rest of the padded plaintext. (A content that
1042 // is longer than the size it was given fails in takeExactly, below.)
1043 while (counter < total - 1) {
1044 const block = chunker.takeExactly(CHUNK_SIZE);
1045 yield { bytes: await sealBlock(key, header, nonceBase, counter, block, false) };
1046 counter++;
1047 }
1048 // The last chunk: what is left of the plaintext, then zeros up to the padded size.
1049 const tail = chunker.takeExactly(lastChunkSize(padded));
1050 yield { bytes: await sealBlock(key, header, nonceBase, counter, tail, true) };
1051}
1052
1053/**
1054 * Seals content: a string or bytes. The result holds the envelope's bytes and its header.
1055 *
1056 * seal('a note', { lock: 'password', password: 'correct horse' })
1057 * seal(bytes, { lock: 'people', keys: [{ subject, publicKey }] })
1058 * seal(bytes, { lock: 'link' }) // the key comes back as linkKey
1059 */
1060async function seal(content, options = {}) {
1061 const bytes = toBytes(content);
1062 const pieces = [];
1063 let header = null;
1064 let headerBytes = null;
1065 let contentKey = null;
1066 for await (const piece of sealPieces(once(bytes), { ...options, size: bytes.length })) {
1067 if (piece.header) { header = piece.header; headerBytes = piece.headerBytes; contentKey = piece.contentKey; }
1068 pieces.push(piece.bytes);
1069 }
1070 const envelope = {
1071 version: VERSION,
1072 header,
1073 headerBytes,
1074 bytes: concat(...pieces),
1075 chunkOffset: 8 + headerBytes.length,
1076 };
1077 if (options.lock === 'link') {
1078 // The link carries the content key; the envelope itself stores nothing.
1079 envelope.linkKey = base64url(contentKey);
1080 }
1081 return envelope;
1082}
1083
1084/** The header's core, as a reader needs it: the fields must be the ones we write. */
1085function readCore(header) {
1086 if (!header || typeof header !== 'object' || Array.isArray(header)) {
1087 fail('locker.bad_envelope', 'The envelope header is not an object');
1088 }
1089 if (header.version !== VERSION) fail('locker.bad_envelope', 'This envelope is another version');
1090 if (header.suite !== SUITE) fail('locker.bad_envelope', 'This envelope uses another suite');
1091 if (header.chunk_size !== CHUNK_SIZE) fail('locker.bad_envelope', 'This envelope uses another chunk size');
1092 if (!Number.isInteger(header.padded_size) || header.padded_size < MIN_PADDED_SIZE) {
1093 fail('locker.bad_envelope', 'The padded size is not a size we write');
1094 }
1095 if (!['people', 'password', 'link'].includes(header.lock)) {
1096 fail('locker.bad_envelope', 'The lock is not one we know');
1097 }
1098 if (!header.slots || typeof header.slots !== 'object') {
1099 fail('locker.bad_envelope', 'The envelope has no slots');
1100 }
1101 return header;
1102}
1103
1104/** The content key out of the header, with the credentials the reader has. */
1105async function openContentKey(header, credentials = {}) {
1106 // A caller that still holds the content key needs no private key: an agent that sealed the item
1107 // can open it, or wrap it for the people who stay, with the key it kept. A string is base64url,
1108 // as the link key travels.
1109 if (credentials.contentKey) {
1110 const given = typeof credentials.contentKey === 'string'
1111 ? fromBase64url(credentials.contentKey) : toBytes(credentials.contentKey);
1112 if (given.length !== KEY_BYTES) fail('locker.invalid', 'The content key is 32 bytes');
1113 return given;
1114 }
1115 if (header.lock === 'people') {
1116 // A person may be named by their key instead of their subject: the slot says which key
1117 // it is for, so the reader does not need to know who they are.
1118 if (credentials.person) {
1119 const slot = await slotForPerson(header, credentials.person);
1120 const privateKey = await importPrivateKey(privateKeyOf(credentials.person) || credentials.privateKey);
1121 if (!privateKey) fail('locker.no_key', 'A people lock needs the private key of a recipient');
1122 return unwrapPeopleSlot(slot, privateKey);
1123 }
1124 const slot = pickSlot(header, credentials.subject);
1125 const privateKey = await importPrivateKey(credentials.privateKey);
1126 if (!privateKey) fail('locker.no_key', 'A people lock needs the private key of a recipient');
1127 return unwrapPeopleSlot(slot, privateKey);
1128 }
1129 if (header.lock === 'password') {
1130 if (!credentials.password) fail('locker.no_key', 'A password lock needs the password');
1131 if (!header.slots.password) fail('locker.bad_envelope', 'The envelope has no password slot');
1132 return unwrapPasswordSlot(header.slots.password, credentials.password);
1133 }
1134 if (!credentials.key) fail('locker.no_key', 'A link lock needs the key from the link');
1135 const key = typeof credentials.key === 'string' ? fromBase64url(credentials.key) : toBytes(credentials.key);
1136 if (key.length !== KEY_BYTES) fail('locker.invalid', 'The link key is 32 bytes');
1137 return key;
1138}
1139
1140/** The slot of the person who is opening: theirs, or the only one there is. */
1141function pickSlot(header, subject) {
1142 const slots = (header.slots && header.slots.people) || [];
1143 if (!slots.length) fail('locker.bad_envelope', 'The envelope has no people slots');
1144 if (subject) {
1145 const id = typeof subject === 'string' ? subject : subject.id;
1146 const type = typeof subject === 'string' ? null : subject.type;
1147 const found = slots.find(slot => slot.subject.id === id && (type === null || slot.subject.type === type));
1148 if (!found) fail('locker.no_key', 'This envelope has no slot for that person');
1149 return found;
1150 }
1151 if (slots.length > 1) fail('locker.no_key', 'This envelope has several slots: say which person you are');
1152 return slots[0];
1153}
1154
1155/** The ECDH public key of a person, as a JWK. */
1156async function publicKeyOf(person) {
1157 if (!person || typeof person !== 'object') fail('locker.invalid', 'A people lock needs the person');
1158 if (person.ecdhPublic) return publicJwk(person.ecdhPublic);
1159 if (person.ecdh && person.ecdh.kty) return publicJwk(person.ecdh);
1160 if (person.kty === 'EC') return publicJwk(person);
1161 if (person.ecdh && person.ecdh.publicKey) {
1162 return publicJwk(await subtle().exportKey('jwk', person.ecdh.publicKey));
1163 }
1164 fail('locker.invalid', 'The person has no ECDH public key');
1165}
1166
1167/** The ECDH private key of a person, if they brought one. */
1168function privateKeyOf(person) {
1169 if (!person || typeof person !== 'object') return null;
1170 if (person.ecdh && person.ecdh.privateKey) return person.ecdh.privateKey;
1171 if (person.ecdhPrivateKey) return person.ecdhPrivateKey;
1172 if (person.privateKey) return person.privateKey;
1173 return null;
1174}
1175
1176/**
1177 * The slot that names this person's key. Every people slot carries the thumbprint of the key
1178 * it was made for, so the reader finds its own slot without knowing anybody's subject. An
1179 * envelope that has no slot for this key is not an envelope this person can open: that is
1180 * locker.no_slot, which says the key is not on the list (not that the key is wrong).
1181 */
1182async function slotForPerson(header, person) {
1183 const slots = (header.slots && header.slots.people) || [];
1184 if (!slots.length) fail('locker.bad_envelope', 'The envelope has no people slots');
1185 const thumbprint = await jwkThumbprint(await publicKeyOf(person));
1186 const found = slots.find(slot => slot.recipient_key === thumbprint);
1187 if (!found) fail('locker.no_slot', 'This envelope has no slot for that key');
1188 return found;
1189}
1190
1191/** A private key as WebCrypto wants it: a CryptoKey, or a JWK to import. */
1192async function importPrivateKey(key) {
1193 if (!key) return null;
1194 if (key.type === 'private' && key.algorithm && key.algorithm.name === 'ECDH') return key;
1195 if (key.kty === 'EC' && key.crv === 'P-256' && key.d) {
1196 try {
1197 return await subtle().importKey('jwk', key, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
1198 } catch {
1199 fail('locker.invalid', 'The private key is not a P-256 key');
1200 }
1201 }
1202 fail('locker.invalid', 'The private key must be an ECDH P-256 key');
1203}
1204
1205/** Reads exact byte counts out of an envelope that is already in memory. */
1206function arrayReader(bytes) {
1207 let at = 0;
1208 return {
1209 /** The next bytes, or null when the envelope is over. */
1210 async probe(count) {
1211 if (at >= bytes.length) return null;
1212 return this.take(count);
1213 },
1214 async take(count) {
1215 if (at + count > bytes.length) fail('locker.truncated', 'The envelope ends inside a chunk');
1216 const out = bytes.slice(at, at + count);
1217 at += count;
1218 return out;
1219 },
1220 };
1221}
1222
1223/** Reads exact byte counts out of a stream, keeping only what it must. */
1224function streamReader(iterator) {
1225 let buffer = new Uint8Array(0);
1226 let ended = false;
1227 async function fill(count) {
1228 while (buffer.length < count && !ended) {
1229 const next = await iterator.next();
1230 if (next.done) { ended = true; break; }
1231 buffer = buffer.length ? concat(buffer, next.value) : next.value;
1232 }
1233 return buffer.length;
1234 }
1235 return {
1236 /** The next bytes, or null when the stream is over. */
1237 async probe(count) {
1238 const have = await fill(count);
1239 if (have === 0) return null;
1240 return this.take(count);
1241 },
1242 async take(count) {
1243 const have = await fill(count);
1244 if (have < count) fail('locker.truncated', 'The envelope ends inside a chunk');
1245 const out = buffer.slice(0, count);
1246 buffer = buffer.slice(count);
1247 return out;
1248 },
1249 };
1250}
1251
1252/**
1253 * Opens the chunks: the plaintext, piece by piece. The reader may be an envelope in memory or a
1254 * stream; both are read the same way, so the same checks run on both.
1255 */
1256async function* openPieces(reader, header, key) {
1257 const expected = chunkCount(header.padded_size);
1258 const lastLength = lastChunkSize(header.padded_size);
1259 const nonceBase = nonceBaseBytes(header);
1260 let counter = 0;
1261 let length = null;
1262 let emitted = 0;
1263 for (;;) {
1264 const lengthBytes = await reader.probe(4);
1265 if (lengthBytes === null) {
1266 if (counter !== expected) fail('locker.truncated', 'The envelope ends before its last chunk');
1267 break;
1268 }
1269 if (counter >= expected) fail('locker.bad_envelope', 'The envelope has more chunks than its size says');
1270 const cipherLength = readU32be(lengthBytes, 0);
1271 // The length is checked before it is used: a header that claims a huge chunk must not
1272 // make the reader wait for it. Every chunk but the last is a whole chunk; the last holds
1273 // what is left of the padded size.
1274 const wantedLength = counter === expected - 1 ? lastLength : CHUNK_SIZE;
1275 if (cipherLength !== wantedLength + TAG_SIZE) {
1276 fail('locker.bad_envelope', 'A chunk is not the size the envelope says');
1277 }
1278 const flags = (await reader.take(1))[0];
1279 const nonce = await reader.take(12);
1280 const ciphertext = await reader.take(cipherLength);
1281 const isFinal = (flags & FINAL_FLAG) === FINAL_FLAG;
1282 if (isFinal !== (counter === expected - 1)) {
1283 fail('locker.bad_envelope', 'The envelope marks the wrong chunk as the last one');
1284 }
1285 const wanted = chunkNonce(nonceBase, counter);
1286 for (let i = 0; i < wanted.length; i++) {
1287 if (nonce[i] !== wanted[i]) fail('locker.bad_envelope', 'A chunk carries the wrong nonce');
1288 }
1289 let plain;
1290 try {
1291 plain = new Uint8Array(await subtle().decrypt(
1292 { name: 'AES-GCM', iv: nonce, additionalData: chunkAad(header, counter, flags) },
1293 key,
1294 ciphertext,
1295 ));
1296 } catch (err) {
1297 if (err && err.code) throw err;
1298 fail('locker.wrong_key', 'This envelope does not open with this key');
1299 }
1300 if (counter === 0) {
1301 if (plain.length < LENGTH_PREFIX) fail('locker.bad_envelope', 'The first chunk is too short');
1302 length = Number(readU64be(plain, 0));
1303 if (length > header.padded_size - LENGTH_PREFIX) {
1304 fail('locker.size_mismatch', 'The content is longer than the envelope says');
1305 }
1306 }
1307 const start = counter === 0 ? LENGTH_PREFIX : 0;
1308 const part = plain.subarray(start);
1309 const room = length - emitted;
1310 const out = part.length > room ? part.subarray(0, room) : part;
1311 emitted += out.length;
1312 counter++;
1313 if (out.length) yield out;
1314 }
1315 if (emitted !== length) fail('locker.size_mismatch', 'The envelope holds fewer bytes than it says');
1316}
1317
1318/**
1319 * Opens an envelope: the content as bytes.
1320 *
1321 * open(envelope, { privateKey, subject }) // a people lock, by subject
1322 * open(envelope, { person }) // a people lock, by key
1323 * open(envelope, { password }) // a password lock
1324 * open(envelope, { key }) // a link lock
1325 */
1326async function open(envelope, credentials = {}) {
1327 const bytes = envelopeBytes(envelope);
1328 const { header, chunkOffset } = readHeader(bytes);
1329 readCore(header);
1330 const contentKey = await openContentKey(header, credentials);
1331 const key = await importContentKey(contentKey, ['decrypt']);
1332 const reader = arrayReader(bytes.subarray(chunkOffset));
1333 const parts = [];
1334 for await (const part of openPieces(reader, header, key)) parts.push(part);
1335 return concat(...parts);
1336}
1337
1338/** A stream of the bytes of a WHATWG stream (or of any async iterable of bytes). */
1339async function* readChunks(stream) {
1340 if (stream && typeof stream.getReader === 'function') {
1341 const reader = stream.getReader();
1342 try {
1343 for (;;) {
1344 const { value, done } = await reader.read();
1345 if (done) return;
1346 yield value instanceof Uint8Array ? value : new Uint8Array(value);
1347 }
1348 } finally {
1349 reader.releaseLock();
1350 }
1351 return;
1352 }
1353 for await (const part of stream) yield part instanceof Uint8Array ? part : new Uint8Array(part);
1354}
1355
1356/**
1357 * Seals a stream: the envelope comes out piece by piece, and only one chunk is ever in memory.
1358 * The size of the content has to be known, because the padded size is in the header and the
1359 * header is written first.
1360 */
1361function sealStream(stream, options = {}) {
1362 if (typeof options.size !== 'number' || options.size < 0) {
1363 fail('locker.invalid', 'A stream seal needs the size of the content');
1364 }
1365 const iterator = sealPieces(readChunks(stream), { ...options, size: options.size })[Symbol.asyncIterator]();
1366 return new ReadableStream({
1367 async pull(controller) {
1368 const next = await iterator.next();
1369 if (next.done) controller.close();
1370 else controller.enqueue(next.value.bytes);
1371 },
1372 async cancel(reason) { if (iterator.return) await iterator.return(reason); },
1373 });
1374}
1375
1376/**
1377 * Opens a stream: the content comes out piece by piece, and only one chunk is ever in memory.
1378 *
1379 * The chunks are yielded as they are verified, so a reader sees the earlier ones before the last
1380 * one is reached. A stream that ends early throws at the end, when the missing chunk shows up:
1381 * discard what it yielded, because the content was never complete.
1382 */
1383function openStream(stream, credentials = {}) {
1384 const iterator = (async function* openStreamPieces() {
1385 const reader = streamReader(readChunks(stream)[Symbol.asyncIterator]());
1386 const head = await reader.probe(8);
1387 if (head === null) fail('locker.truncated', 'The envelope is too short to hold a header');
1388 if (fromUtf8(head.subarray(0, 4)) !== MAGIC) fail('locker.bad_envelope', 'These bytes are not a Locker envelope');
1389 const headerLength = readU32be(head, 4);
1390 if (headerLength === 0 || headerLength > 65536) {
1391 fail('locker.bad_envelope', `The envelope header is ${headerLength} bytes, which is not one we write`);
1392 }
1393 const headerBytes = await reader.take(headerLength);
1394 let header;
1395 try { header = JSON.parse(fromUtf8(headerBytes)); }
1396 catch { fail('locker.bad_envelope', 'The envelope header is not JSON'); }
1397 readCore(header);
1398 const contentKey = await openContentKey(header, credentials);
1399 const key = await importContentKey(contentKey, ['decrypt']);
1400 yield* openPieces(reader, header, key);
1401 })()[Symbol.asyncIterator]();
1402 return new ReadableStream({
1403 async pull(controller) {
1404 const next = await iterator.next();
1405 if (next.done) controller.close();
1406 else controller.enqueue(next.value);
1407 },
1408 async cancel(reason) { if (iterator.return) await iterator.return(reason); },
1409 });
1410}
1411
1412/**
1413 * Wraps the content key for other people, without touching the content: the envelope's header
1414 * changes, the chunks do not, because the slots are not part of what the chunks authenticate.
1415 * This is how a member is added to a people-locked item: the people who are already on it keep
1416 * their slots, and the new ones get one of their own.
1417 *
1418 * Removing a member wants a fresh content key, not just a slot that is gone: the person who is
1419 * removed may still hold the old key. So a removal opens the content and seals it again, with a
1420 * new key and a new nonce base, for the people who are left. Their public keys are needed for
1421 * that, so the caller gives them in `keys`.
1422 */
1423async function rewrap(envelope, {
1424 add = [], remove = [], keys = [], credentials = {}, random = platformRandom, ephemeral,
1425} = {}) {
1426 const bytes = envelopeBytes(envelope);
1427 const { header, chunkOffset } = readHeader(bytes);
1428 if (header.lock !== 'people') fail('locker.invalid', 'Only a people-locked envelope can be re-wrapped');
1429 readCore(header);
1430 const gone = new Set(remove.map(who => (typeof who === 'string' ? who : who.id)));
1431 const staying = (header.slots.people || []).filter(slot => !gone.has(slot.subject.id));
1432
1433 if (!gone.size) {
1434 // Adding: the content key and the chunks stay as they are.
1435 const contentKey = await openContentKey(header, credentials);
1436 const people = staying.slice();
1437 if (add.length) {
1438 const pair = await resolveEphemeral(ephemeral);
1439 for (const recipient of add) {
1440 people.push(await wrapFor(contentKey, recipient.publicKey, {
1441 subject: recipient.subject, ephemeral: pair, random,
1442 }));
1443 }
1444 }
1445 if (!people.length) fail('locker.invalid', 'A people-locked envelope needs at least one slot');
1446 const newHeader = { ...coreOf(header), slots: { people } };
1447 const headerBytes = encodeHeader(newHeader);
1448 return {
1449 version: VERSION,
1450 header: newHeader,
1451 headerBytes,
1452 bytes: encodeEnvelope(headerBytes, bytes.subarray(chunkOffset)),
1453 chunkOffset: 8 + headerBytes.length,
1454 };
1455 }
1456
1457 // Removing: the content is opened and sealed again under a new key, so the key the person
1458 // who is leaving still holds opens nothing.
1459 const contentKey = await openContentKey(header, credentials);
1460 const plain = await open(bytes, credentials);
1461 const recipients = [];
1462 for (const slot of staying) {
1463 const found = keys.find(key => (typeof key.subject === 'string' ? key.subject : key.subject.id) === slot.subject.id);
1464 if (!found) {
1465 fail('locker.invalid', `Removing a person needs the public key of ${slot.subject.id}, who stays`);
1466 }
1467 recipients.push({ subject: slot.subject, publicKey: found.publicKey });
1468 }
1469 for (const recipient of add) recipients.push({ subject: recipient.subject, publicKey: recipient.publicKey });
1470 if (!recipients.length) fail('locker.invalid', 'A people-locked envelope needs at least one slot');
1471 return seal(plain, { lock: 'people', keys: recipients, random, ephemeral });
1472}
1473
1474__ovLockerModule.exports = {
1475 seal, open, sealStream, openStream, rewrap,
1476 envelopeBytes, toBytes, openContentKey, readCore, prepare, makeHeader, Chunker,
1477 publicKeyOf, privateKeyOf, slotForPerson,
1478};
1479
1480 },
1481 "server": function (__ovLockerModule, exports, __ovLockerReq) {
1482'use strict';
1483/**
1484 * What a service needs to know about an envelope without opening it: the header's shape and the
1485 * envelope's size. Nothing here decrypts anything, and nothing here needs a key: a service may
1486 * hold the bytes of an item it can never read.
1487 *
1488 * A service that stores an envelope should:
1489 * - check it with inspectEnvelope() and refuse what does not pass;
1490 * - keep the header as it is (it holds no key material, only wrapped keys);
1491 * - skip indexing, previews, thumbnails, transcoding and content AI;
1492 * - show the lock badge, and serve the bytes only to the people canView allows.
1493 */
1494const { fromUtf8, fromBase64, readU32be } = __ovLockerReq('./bytes');
1495const {
1496 MAGIC, VERSION, SUITE, CHUNK_SIZE, TAG_SIZE, NONCE_SIZE, NONCE_BASE_SIZE, MIN_PADDED_SIZE,
1497 MAX_HEADER_BYTES, chunkCount, lastChunkSize,
1498} = __ovLockerReq('./format');
1499
1500/** The bytes of a base64 field, or null when it is not base64 of that many bytes. */
1501function bytesOf(text, length) {
1502 if (typeof text !== 'string' || !text.length) return null;
1503 if (!/^[A-Za-z0-9+/]+={0,2}$/.test(text)) return null;
1504 let bytes;
1505 try { bytes = fromBase64(text); } catch { return null; }
1506 return bytes.length === length ? bytes : null;
1507}
1508
1509/** A subject: { type, id }, both there. */
1510function subjectOk(subject) {
1511 return !!subject && typeof subject === 'object' && typeof subject.type === 'string'
1512 && typeof subject.id === 'string' && subject.type.length > 0 && subject.id.length > 0;
1513}
1514
1515/** A public key as a JWK: P-256, with its two coordinates. */
1516function publicKeyOk(key) {
1517 return !!key && typeof key === 'object' && key.kty === 'EC' && key.crv === 'P-256'
1518 && typeof key.x === 'string' && /^[A-Za-z0-9_-]{43}$/.test(key.x)
1519 && typeof key.y === 'string' && /^[A-Za-z0-9_-]{43}$/.test(key.y);
1520}
1521
1522/** One people slot: whose it is, which key it is for, and the wrapped key with the bytes it must have. */
1523function peopleSlotOk(slot) {
1524 if (!slot || typeof slot !== 'object') return false;
1525 if (!subjectOk(slot.subject)) return false;
1526 if (slot.algorithm !== 'ECDH-P256/HKDF-SHA256/AES-256-GCM') return false;
1527 // The thumbprint of the recipient's key: the slot names the key it is for.
1528 if (typeof slot.recipient_key !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(slot.recipient_key)) return false;
1529 if (!publicKeyOk(slot.ephemeral_public_key)) return false;
1530 if (!bytesOf(slot.salt, 16)) return false;
1531 if (!bytesOf(slot.nonce, NONCE_SIZE)) return false;
1532 return !!bytesOf(slot.wrapped_key, 48);
1533}
1534
1535/** The password slot: the parameters and the wrapped key. */
1536function passwordSlotOk(slot) {
1537 if (!slot || typeof slot !== 'object') return false;
1538 if (slot.algorithm !== 'PBKDF2-SHA256/AES-256-GCM') return false;
1539 if (slot.kdf !== 'PBKDF2-SHA-256') return false;
1540 if (!Number.isInteger(slot.iterations) || slot.iterations < 600000 || slot.iterations > 10000000) return false;
1541 if (!bytesOf(slot.salt, 16)) return false;
1542 if (!bytesOf(slot.nonce, NONCE_SIZE)) return false;
1543 return !!bytesOf(slot.wrapped_key, 48);
1544}
1545
1546/**
1547 * The header's shape: what it says, and whether the slots match the lock it names. The problems
1548 * are returned as words, so a service can log them; nothing is thrown and nothing is decrypted.
1549 */
1550function checkHeader(header) {
1551 const errors = [];
1552 if (!header || typeof header !== 'object' || Array.isArray(header)) {
1553 return ['the header is not an object'];
1554 }
1555 if (header.version !== VERSION) errors.push(`the version is not ${VERSION}`);
1556 if (header.suite !== SUITE) errors.push(`the suite is not ${SUITE}`);
1557 if (header.chunk_size !== CHUNK_SIZE) errors.push(`the chunk size is not ${CHUNK_SIZE}`);
1558 if (!Number.isInteger(header.padded_size) || header.padded_size < MIN_PADDED_SIZE) {
1559 errors.push(`the padded size is not at least ${MIN_PADDED_SIZE} bytes`);
1560 }
1561 if (!bytesOf(header.nonce_base, NONCE_BASE_SIZE)) errors.push('the nonce base is not eight bytes');
1562 if (!['people', 'password', 'link'].includes(header.lock)) errors.push('the lock is not one we know');
1563 const slots = header.slots;
1564 if (!slots || typeof slots !== 'object' || Array.isArray(slots)) {
1565 errors.push('the header has no slots');
1566 return errors;
1567 }
1568 const kinds = Object.keys(slots);
1569 const wanted = header.lock === 'people' ? 'people' : header.lock;
1570 if (kinds.length !== 1 || kinds[0] !== wanted) {
1571 errors.push(`the slots do not match the ${header.lock} lock`);
1572 return errors;
1573 }
1574 if (wanted === 'people') {
1575 if (!Array.isArray(slots.people) || !slots.people.length || slots.people.length > 100) {
1576 errors.push('the people slots are not a list of one to a hundred');
1577 } else if (!slots.people.every(peopleSlotOk)) {
1578 errors.push('a people slot is not the shape it must be');
1579 }
1580 } else if (wanted === 'password') {
1581 if (!passwordSlotOk(slots.password)) errors.push('the password slot is not the shape it must be');
1582 } else if (Object.keys(slots.link).length) {
1583 errors.push('a link slot stores nothing, and this one does');
1584 }
1585 return errors;
1586}
1587
1588/**
1589 * Reads an envelope's header and checks its shape and its size. Nothing is decrypted and no key
1590 * is needed: this is what a service runs on bytes it may never be able to read.
1591 */
1592function inspectBytes(bytes, { maxHeaderBytes = MAX_HEADER_BYTES } = {}, totalBytes = bytes && bytes.length) {
1593 const errors = [];
1594 const data = bytes instanceof Uint8Array ? bytes : null;
1595 if (!data) return { valid: false, errors: ['the envelope is not bytes'], header: null };
1596 if (data.length < 8) return { valid: false, errors: ['the envelope is too short to hold a header'], header: null };
1597 if (fromUtf8(data.subarray(0, 4)) !== MAGIC) {
1598 return { valid: false, errors: ['these bytes are not a Locker envelope'], header: null };
1599 }
1600 const headerLength = readU32be(data, 4);
1601 if (headerLength === 0 || headerLength > maxHeaderBytes) {
1602 return { valid: false, errors: [`the envelope header is ${headerLength} bytes, which is not one we write`], header: null };
1603 }
1604 if (data.length < 8 + headerLength) {
1605 return { valid: false, errors: ['the envelope ends inside its header'], header: null };
1606 }
1607 let header = null;
1608 try { header = JSON.parse(fromUtf8(data.subarray(8, 8 + headerLength))); }
1609 catch { return { valid: false, errors: ['the envelope header is not JSON'], header: null }; }
1610 if (!header || typeof header !== 'object' || Array.isArray(header)) return { valid: false, errors: ['the header is not an object'], header: null };
1611 try { errors.push(...checkHeader(header)); } catch { errors.push('the slots are not an object'); }
1612 const chunkOffset = 8 + headerLength;
1613 let expectedBytes = null;
1614 if (Number.isInteger(header.padded_size) && header.padded_size >= MIN_PADDED_SIZE
1615 && header.chunk_size === CHUNK_SIZE) {
1616 const chunks = chunkCount(header.padded_size);
1617 // Every chunk: its length, its flags, its nonce, the ciphertext and the tag. Every chunk
1618 // but the last is a whole chunk; the last holds what is left of the padded size.
1619 const frame = 4 + 1 + NONCE_SIZE;
1620 const whole = (chunks - 1) * (frame + CHUNK_SIZE + TAG_SIZE);
1621 const last = frame + lastChunkSize(header.padded_size) + TAG_SIZE;
1622 expectedBytes = chunkOffset + whole + last;
1623 if (totalBytes < expectedBytes) errors.push('the envelope is shorter than its own header says');
1624 }
1625 return {
1626 valid: errors.length === 0,
1627 errors,
1628 header,
1629 headerLength,
1630 chunkOffset,
1631 lock: header.lock,
1632 paddedSize: header.padded_size,
1633 chunkCount: Number.isInteger(header.padded_size) ? chunkCount(header.padded_size) : null,
1634 expectedBytes,
1635 };
1636}
1637
1638function inspectEnvelope(bytes, options) { return inspectBytes(bytes, options); }
1639
1640/** A streamed upload: inspect just its bounded header against the ACTUAL stored byte count.
1641 * The caller must have counted and bounded the complete ciphertext first, not trust Content-Length.
1642 * No chunks are read or authenticated here; as with inspectEnvelope, only a client can do that.
1643 */
1644function inspectEnvelopePrefix(prefix, totalBytes, options) {
1645 const info = inspectBytes(prefix, options, totalBytes);
1646 if (!Number.isSafeInteger(totalBytes) || !Number.isSafeInteger(info.expectedBytes) || totalBytes !== info.expectedBytes) {
1647 info.errors.push('the stored byte count does not match the envelope');
1648 }
1649 if (info.header && !Number.isSafeInteger(info.header.padded_size)) info.errors.push('the padded size is not a safe integer');
1650 info.valid = info.errors.length === 0;
1651 return info;
1652}
1653__ovLockerModule.exports = { inspectEnvelope, inspectEnvelopePrefix, checkHeader, peopleSlotOk };
1654
1655 },
1656 "words": function (__ovLockerModule, exports, __ovLockerReq) {
1657'use strict';
1658/**
1659 * The 256 words of a fingerprint. A fingerprint is the first eight bytes of SHA-256 over a
1660 * person's two public keys, read as eight words in two groups of four: eight bits each, so the
1661 * words say which of the 256 they are. It is short enough to read out loud and compare once,
1662 * which is the whole point: a person checks that the eight words on the screen are the eight
1663 * words the other person sees.
1664 *
1665 * The order is part of the format: word number n is the nth word here. Changing the list would
1666 * change every fingerprint, so it is frozen with the version.
1667 */
1668const WORDS = [
1669 'amber', 'anchor', 'apple', 'arrow', 'aspen', 'autumn', 'badge', 'bamboo',
1670 'beacon', 'berry', 'birch', 'bison', 'blossom', 'breeze', 'brick', 'brook',
1671 'bucket', 'bugle', 'cabin', 'cactus', 'candle', 'canoe', 'canyon', 'cedar',
1672 'cellar', 'chalk', 'cherry', 'cider', 'cinder', 'citrus', 'clay', 'clover',
1673 'cobalt', 'cocoa', 'comet', 'coral', 'cosmos', 'cotton', 'cove', 'crane',
1674 'crater', 'crayon', 'cricket', 'crimson', 'crystal', 'cymbal', 'daisy', 'dapple',
1675 'dawn', 'delta', 'desert', 'diamond', 'dolphin', 'domino', 'donkey', 'dragon',
1676 'drift', 'drizzle', 'drum', 'dune', 'dusk', 'eagle', 'ember', 'emerald',
1677 'estuary', 'fable', 'falcon', 'feather', 'fern', 'ferry', 'field', 'flame',
1678 'flint', 'forest', 'fossil', 'fox', 'frost', 'galaxy', 'garden', 'garnet',
1679 'gazelle', 'geyser', 'ginger', 'glacier', 'granite', 'grape', 'gravel', 'grove',
1680 'guitar', 'haiku', 'harbor', 'hazel', 'heather', 'hollow', 'honey', 'horizon',
1681 'ice', 'indigo', 'island', 'ivory', 'jasper', 'jungle', 'juniper', 'kayak',
1682 'kernel', 'kestrel', 'lagoon', 'lantern', 'lattice', 'laurel', 'lemon', 'lichen',
1683 'lilac', 'linen', 'lotus', 'lunar', 'magnet', 'mango', 'maple', 'marble',
1684 'marsh', 'meadow', 'meteor', 'mint', 'mirror', 'mist', 'monsoon', 'moss',
1685 'moth', 'mountain', 'nectar', 'needle', 'nest', 'nettle', 'night', 'oasis',
1686 'ocean', 'olive', 'onyx', 'opal', 'orbit', 'orchard', 'orchid', 'osprey',
1687 'otter', 'owl', 'oyster', 'palace', 'palm', 'panda', 'papaya', 'paper',
1688 'peach', 'pearl', 'pebble', 'pecan', 'penguin', 'peony', 'pepper', 'petal',
1689 'pine', 'plateau', 'plum', 'pollen', 'pond', 'poplar', 'poppy', 'prairie',
1690 'prism', 'pumpkin', 'quail', 'quarry', 'quartz', 'rabbit', 'radish', 'rain',
1691 'raven', 'reef', 'ridge', 'river', 'robin', 'rosemary', 'sage', 'salmon',
1692 'sand', 'sapphire', 'satellite', 'scallop', 'shadow', 'shell', 'shore', 'shuttle',
1693 'silk', 'silver', 'slate', 'snow', 'solar', 'sparrow', 'spruce', 'star',
1694 'stone', 'storm', 'stream', 'sugar', 'summit', 'sunset', 'swan', 'thistle',
1695 'thunder', 'tide', 'tiger', 'timber', 'topaz', 'trail', 'tulip', 'tundra',
1696 'turtle', 'valley', 'vapor', 'velvet', 'vine', 'violet', 'walnut', 'wave',
1697 'whale', 'wheat', 'willow', 'winter', 'wolf', 'wren', 'yacht', 'yarrow',
1698 'yew', 'zinc', 'zephyr', 'almond', 'basil', 'cinnamon', 'cypress', 'damson',
1699 'elder', 'fennel', 'garlic', 'hibiscus', 'iris', 'jasmine', 'linden', 'melon',
1700 'nutmeg', 'sesame', 'sorrel', 'thyme', 'anise', 'chicory', 'dill', 'endive',
1701];
1702
1703__ovLockerModule.exports = { WORDS };
1704
1705 },
1706 };
1707 const cache = {};
1708 function req(name) {
1709 const key = String(name).replace(/^\.\//, '');
1710 if (!defs[key]) throw new Error('ov-locker-core: no module ' + name);
1711 if (!cache[key]) { const m = { exports: {} }; cache[key] = m; defs[key](m, m.exports, req); }
1712 return cache[key].exports;
1713 }
1714 root.OVLockerCore = Object.assign({}, req('index'), { SDK_VERSION: "0.160.0" });
1715})(typeof globalThis !== 'undefined' ? globalThis : this);
1716