@secrecy/lib 1.86.2-fix-metrics.1 → 1.86.2-fix-md5.1

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.
@@ -702,8 +702,10 @@ export class SecrecyCloudClient {
702
702
  const src = key
703
703
  ? await decrypt(key, encryptedContent, progressDecrypt, signal)
704
704
  : encryptedContent;
705
- const md5Content = await md5(src);
706
- if (md5Content !== dataContent.md5) {
705
+ // Encrypted data is authenticated by the stream: only public data has an md5.
706
+ if (!key &&
707
+ dataContent.md5 !== null &&
708
+ (await md5(src)) !== dataContent.md5) {
707
709
  throw new Error(`Content does not match`);
708
710
  }
709
711
  const decompressed = decompress(src);
@@ -1,14 +1,13 @@
1
1
  import z from 'zod';
2
2
  import { buildBytesFromDataLink } from '../crypto/domain';
3
3
  import { sodium } from '../sodium';
4
- import { derivePassword } from '../crypto/helpers';
5
- import { decryptSecretBox } from '../crypto';
4
+ import { decryptKeyWithPassword } from '../crypto/helpers';
6
5
  import { decrypt } from '../worker/sodium';
7
- import { md5 } from '../worker/md5';
8
6
  import { decompress } from '../minify';
9
7
  const downloadDataLinkS3Schema = z.object({
10
8
  name: z.string(),
11
- md5: z.string(),
9
+ // Only for unencrypted and legacy encrypted data.
10
+ md5: z.string().nullable(),
12
11
  md5Encrypted: z.string().nullable(),
13
12
  size: z.coerce.bigint().gte(0n),
14
13
  parts: z.array(z.object({ order: z.number(), md5: z.string(), contentUrl: z.string() })),
@@ -17,7 +16,8 @@ const downloadDataLinkS3Schema = z.object({
17
16
  });
18
17
  const downloadDataLinkLiteSchema = z.object({
19
18
  name: z.string(),
20
- md5: z.string(),
19
+ // Only for unencrypted and legacy encrypted data.
20
+ md5: z.string().nullable(),
21
21
  md5Encrypted: z.string().nullable(),
22
22
  size: z.coerce.bigint().gte(0n),
23
23
  bytes: z.string(),
@@ -69,14 +69,8 @@ export async function downloadDataFromLink(opts) {
69
69
  throw new Error('Unable to read encrypted data without password!');
70
70
  }
71
71
  try {
72
- const salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, opts.crypto.password + dataLink.md5);
73
- const derivedPassword = derivePassword(opts.crypto.password, salt);
74
- const decryptedSharingDataKey = decryptSecretBox(sodium.from_hex(opts.crypto.key), derivedPassword);
72
+ const decryptedSharingDataKey = decryptKeyWithPassword(sodium.from_hex(opts.crypto.key), opts.crypto.password);
75
73
  const decryptedBytes = await decrypt(decryptedSharingDataKey, bytes, opts.decryptProgress, opts.signal);
76
- const md5Content = await md5(decryptedBytes);
77
- if (md5Content !== dataLink.md5) {
78
- throw new Error(`Content does not match`);
79
- }
80
74
  return decompress(decryptedBytes);
81
75
  }
82
76
  catch (err) {
@@ -9,7 +9,9 @@ export function parseInfos() {
9
9
  const hash = window.location.hash.substr(1);
10
10
  try {
11
11
  const res = JSON.parse(atob(hash));
12
- window.location.hash = '';
12
+ // Replace (not push) the entry: the fragment holds the private keys and
13
+ // must not stay in the browser history.
14
+ window.history.replaceState(window.history.state, '', window.location.pathname + window.location.search);
13
15
  const data = secrecyUserApp.safeParse(res);
14
16
  return data.success ? data.data : null;
15
17
  }
@@ -8,8 +8,8 @@ import { chunks, enumerate } from '../utils/array.js';
8
8
  import { getTrpcGuestClient, } from '../client.js';
9
9
  import axios from 'axios';
10
10
  import { encryptDataAndKey } from '../crypto/domain.js';
11
- import { derivePassword, generatePassword } from '../crypto/helpers.js';
12
- import { decryptCryptoBox, encryptSecretBox } from '../crypto/index.js';
11
+ import { encryptKeyWithPassword, generatePassword } from '../crypto/helpers.js';
12
+ import { decryptCryptoBox } from '../crypto/index.js';
13
13
  export async function uploadData({ storageType, data, password, forcePassword = false, encrypted = true, encryptProgress, uploadProgress, signal, meta, currentIdentity, keyPairs, apiClient, }) {
14
14
  if (!encrypted && (password || forcePassword)) {
15
15
  throw new Error('Cannot share unencrypted data with a password!');
@@ -40,7 +40,10 @@ export async function uploadData({ storageType, data, password, forcePassword =
40
40
  throw new Error('Cold storage is only for logged users!');
41
41
  }
42
42
  const compressed = encrypted ? compress(dataBuffer) : dataBuffer;
43
- let { encryptedData, encryptedDataKey, dataKey, md5Data, md5Encrypted } = encrypted
43
+ // Only unencrypted (public) data sends the md5 of its content, computed in
44
+ // the requests below: for encrypted data it would let the server
45
+ // fingerprint the plaintext.
46
+ let { encryptedData, encryptedDataKey, dataKey, md5Encrypted } = encrypted
44
47
  ? await encryptDataAndKey({
45
48
  data: compressed,
46
49
  progress: encryptProgress,
@@ -52,7 +55,6 @@ export async function uploadData({ storageType, data, password, forcePassword =
52
55
  encryptedData: compressed,
53
56
  encryptedDataKey: null,
54
57
  dataKey: null,
55
- md5Data: await md5(compressed),
56
58
  md5Encrypted: null,
57
59
  };
58
60
  await uploadProgress?.({
@@ -63,8 +65,6 @@ export async function uploadData({ storageType, data, password, forcePassword =
63
65
  const createSharing = ({ data, }) => {
64
66
  if (dataKey && (password || forcePassword)) {
65
67
  password ??= generatePassword();
66
- const salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, password + md5Data);
67
- const derivedPassword = derivePassword(password, salt);
68
68
  let key;
69
69
  if (data.type === 'guest' ||
70
70
  (data.type === 'authed' &&
@@ -88,7 +88,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
88
88
  // NOTE: Process to create a sharing for a auth client (todo: endpoint)
89
89
  return {
90
90
  password,
91
- encryptedDataKey: sodium.to_hex(encryptSecretBox(key, derivedPassword)),
91
+ encryptedDataKey: sodium.to_hex(encryptKeyWithPassword(key, password)),
92
92
  };
93
93
  }
94
94
  return null;
@@ -102,14 +102,13 @@ export async function uploadData({ storageType, data, password, forcePassword =
102
102
  size: BigInt(dataBuffer.byteLength),
103
103
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
104
104
  md5Encrypted,
105
- md5: md5Data,
106
105
  fromIdentityPubKey: currentIdentity?.identityPubKey,
107
106
  ...filetype,
108
107
  }
109
108
  : {
110
109
  type: 'unencrypted',
111
110
  content: Buffer.from(encryptedData),
112
- md5: md5Data,
111
+ md5: await md5(compressed),
113
112
  size: BigInt(dataBuffer.byteLength),
114
113
  fromIdentityPubKey: currentIdentity?.identityPubKey,
115
114
  ...filetype,
@@ -124,7 +123,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
124
123
  const localData = {
125
124
  id: uploadData.id,
126
125
  storageType: 'lite',
127
- md5: uploadDataArgs.md5,
126
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
128
127
  size: uploadDataArgs.size,
129
128
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
130
129
  ? uploadDataArgs.sizeEncrypted
@@ -146,13 +145,12 @@ export async function uploadData({ storageType, data, password, forcePassword =
146
145
  size: BigInt(dataBuffer.byteLength),
147
146
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
148
147
  md5Encrypted,
149
- md5: md5Data,
150
148
  fromIdentityPubKey: currentIdentity?.identityPubKey,
151
149
  ...filetype,
152
150
  }
153
151
  : {
154
152
  type: 'unencrypted',
155
- md5: md5Data,
153
+ md5: await md5(compressed),
156
154
  size: BigInt(dataBuffer.byteLength),
157
155
  fromIdentityPubKey: currentIdentity?.identityPubKey,
158
156
  ...filetype,
@@ -173,7 +171,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
173
171
  const localData = {
174
172
  id: uploadData.id,
175
173
  storageType: storageType,
176
- md5: uploadDataArgs.md5,
174
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
177
175
  size: uploadDataArgs.size,
178
176
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
179
177
  ? uploadDataArgs.sizeEncrypted
@@ -257,7 +255,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
257
255
  const localData = {
258
256
  id: uploadData.id,
259
257
  storageType: storageType,
260
- md5: uploadDataArgs.md5,
258
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
261
259
  size: uploadDataArgs.size,
262
260
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
263
261
  ? uploadDataArgs.sizeEncrypted
@@ -1,6 +1,5 @@
1
1
  import { setup, sodium } from '../sodium.js';
2
2
  import SparkMD5 from 'spark-md5';
3
- import { chunks } from '../utils/array.js';
4
3
  function assert(c, message) {
5
4
  if (!c) {
6
5
  throw new Error(message);
@@ -9,7 +8,19 @@ function assert(c, message) {
9
8
  export function secretStreamKeygen() {
10
9
  return sodium.crypto_secretstream_xchacha20poly1305_keygen();
11
10
  }
11
+ function assertKey(key) {
12
+ assert(key.byteLength === sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES, `key must be KEYBYTES (${sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES}) long`);
13
+ }
14
+ // The wrappers return the secretstream state as a raw address in the wasm
15
+ // heap and never release it: wipe it (it holds the stream key) and free it.
16
+ function wipeState(state) {
17
+ const lib = sodium.libsodium;
18
+ const size = lib._crypto_secretstream_xchacha20poly1305_statebytes();
19
+ lib.HEAPU8.fill(0, state, state + size);
20
+ lib._free(state);
21
+ }
12
22
  function encrypt(key) {
23
+ assertKey(key);
13
24
  let destroyed = false;
14
25
  const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
15
26
  const encrypt = (tag, plaintext) => {
@@ -19,6 +30,7 @@ function encrypt(key) {
19
30
  function destroy() {
20
31
  assert(!destroyed, 'state already destroyed');
21
32
  destroyed = true;
33
+ wipeState(state);
22
34
  }
23
35
  return {
24
36
  encrypt,
@@ -27,9 +39,9 @@ function encrypt(key) {
27
39
  };
28
40
  }
29
41
  function decrypt(header, key) {
30
- assert(header.byteLength >=
31
- sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES, `header must be at least HEADERBYTES (${sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES}) long`);
32
- assert(key.byteLength >= sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES, `key must be at least KEYBYTES (${sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES}) long`);
42
+ assert(header.byteLength ===
43
+ sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES, `header must be HEADERBYTES (${sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES}) long`);
44
+ assertKey(key);
33
45
  let destroyed = false;
34
46
  const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
35
47
  const decrypt = (ciphertext) => {
@@ -39,6 +51,7 @@ function decrypt(header, key) {
39
51
  function destroy() {
40
52
  assert(!destroyed, 'state already destroyed');
41
53
  destroyed = true;
54
+ wipeState(state);
42
55
  }
43
56
  return {
44
57
  decrypt,
@@ -46,104 +59,146 @@ function decrypt(header, key) {
46
59
  };
47
60
  }
48
61
  export const CHUNK_SIZE = 8192;
62
+ /**
63
+ * Encrypt `data` as a secretstream: the header followed by one authenticated
64
+ * chunk per CHUNK_SIZE bytes of plaintext. The last chunk (an empty one for
65
+ * empty data) is always tagged `TAG_FINAL` so that truncation is detected.
66
+ */
49
67
  export async function encryptSecretStream(key, data, progress, signal) {
50
68
  await setup();
51
69
  const { encrypt: crypt, destroy, header } = encrypt(key);
52
- const encryptedChunk = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
53
- const max = Math.ceil(data.byteLength / CHUNK_SIZE) * encryptedChunk + header.byteLength;
54
- void progress?.({
55
- percent: 0,
56
- total: max,
57
- current: 0,
58
- });
59
- const final = new Uint8Array(max);
60
- const sparkEncrypted = new SparkMD5.ArrayBuffer();
61
- const spark = new SparkMD5.ArrayBuffer();
62
- final.set(header);
63
- sparkEncrypted.append(header.buffer);
64
- let total = header.byteLength;
65
- void progress?.({
66
- percent: total / max,
67
- total: max,
68
- current: total,
69
- });
70
- let lastPercent = total / max;
71
- for (const chunk of chunks(data, CHUNK_SIZE)) {
72
- if (signal?.aborted === true) {
73
- throw new Error(`Encrypt aborted`);
74
- }
75
- spark.append(chunk.buffer);
76
- const tag = chunk.length < CHUNK_SIZE
77
- ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
78
- : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
79
- const encrypted = crypt(tag, chunk);
80
- sparkEncrypted.append(encrypted.buffer);
81
- final.set(encrypted, total);
82
- total += encrypted.byteLength;
83
- const percent = total / max;
84
- if (percent > lastPercent + 0.01) {
85
- void progress?.({
86
- percent,
87
- total: max,
88
- current: total,
89
- });
90
- lastPercent = percent;
70
+ try {
71
+ const chunkCount = Math.max(1, Math.ceil(data.byteLength / CHUNK_SIZE));
72
+ const max = header.byteLength +
73
+ data.byteLength +
74
+ chunkCount * sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
75
+ void progress?.({
76
+ percent: 0,
77
+ total: max,
78
+ current: 0,
79
+ });
80
+ const final = new Uint8Array(max);
81
+ const sparkEncrypted = new SparkMD5.ArrayBuffer();
82
+ final.set(header);
83
+ sparkEncrypted.append(header.buffer);
84
+ let total = header.byteLength;
85
+ void progress?.({
86
+ percent: total / max,
87
+ total: max,
88
+ current: total,
89
+ });
90
+ let lastPercent = total / max;
91
+ for (let i = 0; i < chunkCount; i++) {
92
+ if (signal?.aborted === true) {
93
+ throw new Error(`Encrypt aborted`);
94
+ }
95
+ const chunk = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
96
+ const tag = i === chunkCount - 1
97
+ ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
98
+ : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
99
+ const encrypted = crypt(tag, chunk);
100
+ sparkEncrypted.append(encrypted.buffer);
101
+ final.set(encrypted, total);
102
+ total += encrypted.byteLength;
103
+ const percent = total / max;
104
+ if (percent > lastPercent + 0.01) {
105
+ void progress?.({
106
+ percent,
107
+ total: max,
108
+ current: total,
109
+ });
110
+ lastPercent = percent;
111
+ }
91
112
  }
113
+ void progress?.({
114
+ percent: 1,
115
+ total,
116
+ current: total,
117
+ });
118
+ return {
119
+ data: final,
120
+ md5Encrypted: sparkEncrypted.end(),
121
+ };
122
+ }
123
+ finally {
124
+ destroy();
92
125
  }
93
- destroy();
94
- void progress?.({
95
- percent: 1,
96
- total,
97
- current: total,
98
- });
99
- return {
100
- data: final.slice(0, total),
101
- md5Encrypted: sparkEncrypted.end(),
102
- md5: spark.end(),
103
- };
104
126
  }
127
+ /**
128
+ * Decrypt a secretstream produced by `encryptSecretStream`. Throws if any
129
+ * chunk fails authentication, if data follows the `TAG_FINAL` chunk, or if the
130
+ * stream ends without it (truncation).
131
+ */
105
132
  export async function decryptSecretStream(key, data, progress, signal) {
106
133
  await setup();
107
- const header = data.slice(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
108
- data = data.slice(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
109
- const { decrypt: decryptFn, destroy } = decrypt(header, key);
134
+ const headerBytes = sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES;
110
135
  const chunkSize = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
111
- const max = Math.ceil(data.byteLength / chunkSize) * CHUNK_SIZE;
112
- void progress?.({
113
- percent: 0,
114
- total: max,
115
- current: 0,
116
- });
117
- const final = new Uint8Array(max);
118
- let total = 0;
119
- let lastPercent = total / max;
120
- for (const chunk of chunks(data, chunkSize)) {
121
- if (signal?.aborted === true) {
122
- throw new Error(`Decrypt aborted`);
123
- }
124
- const messageTag = decryptFn(chunk);
125
- if (typeof messageTag === 'boolean') {
126
- throw new Error('Unable to decrypt the data!');
136
+ // A valid stream always holds at least one (possibly empty) chunk.
137
+ if (data.byteLength <
138
+ headerBytes + sodium.crypto_secretstream_xchacha20poly1305_ABYTES) {
139
+ throw new Error('Unable to decrypt the data: the data is too short!');
140
+ }
141
+ const header = data.slice(0, headerBytes);
142
+ const body = data.subarray(headerBytes);
143
+ const chunkCount = Math.ceil(body.byteLength / chunkSize);
144
+ const max = body.byteLength -
145
+ chunkCount * sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
146
+ const { decrypt: decryptFn, destroy } = decrypt(header, key);
147
+ try {
148
+ void progress?.({
149
+ percent: 0,
150
+ total: max,
151
+ current: 0,
152
+ });
153
+ const final = new Uint8Array(max);
154
+ let total = 0;
155
+ let finalTagSeen = false;
156
+ let lastPercent = total / max;
157
+ for (let i = 0; i < chunkCount; i++) {
158
+ if (signal?.aborted === true) {
159
+ throw new Error(`Decrypt aborted`);
160
+ }
161
+ const chunk = body.subarray(i * chunkSize, (i + 1) * chunkSize);
162
+ if (finalTagSeen) {
163
+ throw new Error('Unable to decrypt the data: unexpected data after the final chunk!');
164
+ }
165
+ const messageTag = decryptFn(chunk);
166
+ if (typeof messageTag === 'boolean') {
167
+ throw new Error('Unable to decrypt the data!');
168
+ }
169
+ if (messageTag.tag ===
170
+ sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
171
+ finalTagSeen = true;
172
+ }
173
+ else if (messageTag.tag !==
174
+ sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE) {
175
+ throw new Error('Unable to decrypt the data: unexpected chunk tag!');
176
+ }
177
+ final.set(messageTag.message, total);
178
+ total += messageTag.message.byteLength;
179
+ const percent = total / max;
180
+ if (percent > lastPercent + 0.01) {
181
+ void progress?.({
182
+ percent,
183
+ total: max,
184
+ current: total,
185
+ });
186
+ lastPercent = percent;
187
+ }
127
188
  }
128
- final.set(messageTag.message, total);
129
- total += messageTag.message.byteLength;
130
- const percent = total / max;
131
- if (percent > lastPercent + 0.01) {
132
- void progress?.({
133
- percent,
134
- total: max,
135
- current: total,
136
- });
137
- lastPercent = percent;
189
+ if (!finalTagSeen) {
190
+ throw new Error('Unable to decrypt the data: the data is truncated!');
138
191
  }
192
+ void progress?.({
193
+ percent: 1,
194
+ total,
195
+ current: total,
196
+ });
197
+ return final;
198
+ }
199
+ finally {
200
+ destroy();
139
201
  }
140
- destroy();
141
- void progress?.({
142
- percent: 1,
143
- total,
144
- current: total,
145
- });
146
- return final.slice(0, total);
147
202
  }
148
203
  // async function mainSecretStream(random: Uint8Array): Promise<vo id> {
149
204
  // const key = secretstreamKeygen();
@@ -11,12 +11,11 @@ import { concatenate } from '../utils/array.js';
11
11
  */
12
12
  export async function encryptDataAndKey({ data, currentIdentity, keyPairs, progress, signal, }) {
13
13
  const dataKey = secretStreamKeygen();
14
- const { data: encryptedData, md5: md5Data, md5Encrypted, } = await encrypt(dataKey, data, progress, signal);
14
+ const { data: encryptedData, md5Encrypted } = await encrypt(dataKey, data, progress, signal);
15
15
  if (!currentIdentity || !keyPairs) {
16
16
  return {
17
17
  encryptedData,
18
18
  dataKey,
19
- md5Data,
20
19
  md5Encrypted,
21
20
  };
22
21
  }
@@ -29,7 +28,6 @@ export async function encryptDataAndKey({ data, currentIdentity, keyPairs, progr
29
28
  encryptedDataKey: encDataKey,
30
29
  encryptedData,
31
30
  dataKey,
32
- md5Data,
33
31
  md5Encrypted,
34
32
  };
35
33
  }
@@ -1,5 +1,6 @@
1
- import { encryptCryptoBox } from '.';
1
+ import { decryptSecretBox, encryptCryptoBox, encryptSecretBox } from '.';
2
2
  import { sodium } from '../sodium';
3
+ import { concatenate } from '../utils/array.js';
3
4
  import { encryptSecretStream, secretStreamKeygen } from './data';
4
5
  export function generatePassword() {
5
6
  return sodium.randombytes_buf(16, 'hex');
@@ -7,6 +8,27 @@ export function generatePassword() {
7
8
  export function derivePassword(password, salt) {
8
9
  return sodium.crypto_pwhash(sodium.crypto_secretbox_KEYBYTES, password, salt, sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, sodium.crypto_pwhash_ALG_ARGON2ID13, 'hex');
9
10
  }
11
+ const PASSWORD_KEY_VERSION = 1;
12
+ /**
13
+ * Wrap a data key under a password, with a random salt:
14
+ * `version (1 byte) || salt || nonce || secretbox(key)`.
15
+ */
16
+ export function encryptKeyWithPassword(key, password) {
17
+ const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
18
+ return concatenate(new Uint8Array([PASSWORD_KEY_VERSION]), salt, encryptSecretBox(key, derivePassword(password, salt)));
19
+ }
20
+ /** Unwrap a data key produced by `encryptKeyWithPassword`. */
21
+ export function decryptKeyWithPassword(wrapped, password) {
22
+ const boxBytes = sodium.crypto_secretbox_NONCEBYTES +
23
+ sodium.crypto_secretbox_MACBYTES +
24
+ sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES;
25
+ if (wrapped.byteLength === 1 + sodium.crypto_pwhash_SALTBYTES + boxBytes &&
26
+ wrapped[0] === PASSWORD_KEY_VERSION) {
27
+ const salt = wrapped.slice(1, 1 + sodium.crypto_pwhash_SALTBYTES);
28
+ return decryptSecretBox(wrapped.slice(1 + sodium.crypto_pwhash_SALTBYTES), derivePassword(password, salt));
29
+ }
30
+ throw new Error('Unsupported password protected key format');
31
+ }
10
32
  export async function encryptName(name, nameKey) {
11
33
  const { data } = await encryptSecretStream(sodium.from_hex(nameKey), sodium.from_string(name));
12
34
  return sodium.to_hex(data);
@@ -10,7 +10,7 @@ export function generateCryptoBoxKeyPair() {
10
10
  }
11
11
  export function decryptCryptoBox(data, publicKeyAlice, privateKeyBob) {
12
12
  if (data.length < sodium.crypto_box_NONCEBYTES + sodium.crypto_box_MACBYTES) {
13
- throw 'data too short';
13
+ throw new Error('data too short');
14
14
  }
15
15
  const nonce = data.slice(0, sodium.crypto_box_NONCEBYTES);
16
16
  const cipher = data.slice(sodium.crypto_box_NONCEBYTES);
@@ -27,7 +27,7 @@ export function encryptSecretBox(data, key) {
27
27
  export function decryptSecretBox(data, key) {
28
28
  if (data.length <
29
29
  sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) {
30
- throw 'data too short';
30
+ throw new Error('data too short');
31
31
  }
32
32
  const nonce = data.slice(0, sodium.crypto_secretbox_NONCEBYTES);
33
33
  const cipher = data.slice(sodium.crypto_secretbox_NONCEBYTES);
@@ -100,21 +100,30 @@ function popupExecute(execute, url, name, options, callback) {
100
100
  //
101
101
  };
102
102
  const optionsString = optionsToString(popupOptions);
103
+ // Only the page we opened may answer: the popup name is predictable, so
104
+ // without these checks any window could inject a response (e.g. an
105
+ // attacker's session and keys on login).
106
+ const expectedOrigin = new URL(url, window.location.href).origin;
103
107
  const win = execute(url, popupName, optionsString);
104
108
  let isMessageSent = false;
105
109
  let interval;
106
110
  function popupCallbackOnce(err, data) {
107
111
  if (!isMessageSent) {
108
112
  isMessageSent = true;
113
+ window.removeEventListener('message', onMessage);
114
+ clearInterval(interval);
109
115
  popupCallback(err, data);
110
116
  }
111
117
  }
112
118
  function onMessage(message) {
113
- const data = message !== undefined ? message.data : undefined;
114
- if (data !== undefined && data.id === popupName) {
119
+ if (win === null ||
120
+ message.source !== win ||
121
+ message.origin !== expectedOrigin) {
122
+ return;
123
+ }
124
+ const data = message.data;
125
+ if (data !== undefined && data !== null && data.id === popupName) {
115
126
  popupCallbackOnce(undefined, data.data);
116
- window.removeEventListener('message', onMessage);
117
- clearInterval(interval);
118
127
  }
119
128
  }
120
129
  window.addEventListener('message', onMessage, false);
@@ -172,9 +181,12 @@ export function popupWithPost(url, postData, name, options, callback) {
172
181
  * Return html that when executed, will trigger the popup to callback with a response
173
182
  *
174
183
  * @param {Object}
184
+ * @param {String} targetOrigin origin of the opener allowed to receive `data`
175
185
  * @return {String}
176
186
  */
177
- export function popupResponse(id, data) {
178
- const jsonData = JSON.stringify({ id, data });
179
- return `<script>window.opener.postMessage(${jsonData}, "*");setTimeout(function() { window.close(); }, 50);</script>`;
187
+ export function popupResponse(id, data, targetOrigin) {
188
+ // Escape "<" so the payload cannot close the script tag.
189
+ const jsonData = JSON.stringify({ id, data }).replaceAll('<', '\\u003c');
190
+ const origin = JSON.stringify(targetOrigin).replaceAll('<', '\\u003c');
191
+ return `<script>window.opener.postMessage(${jsonData}, ${origin});setTimeout(function() { window.close(); }, 50);</script>`;
180
192
  }
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-workers.ts, do not edit.
2
+ export const workerMd5Script = "(()=>{var D=Object.create;var{getPrototypeOf:E,defineProperty:F,getOwnPropertyNames:G}=Object;var v=Object.prototype.hasOwnProperty;function H(o){return this[o]}var K,N,R=(o,u,s)=>{var _=o!=null&&typeof o===\"object\";if(_){var y=u?K??=new WeakMap:N??=new WeakMap,b=y.get(o);if(b)return b}s=o!=null?D(E(o)):{};let l=u||!o||!o.__esModule||!v.call(o,\"default\")?F(s,\"default\",{value:o,enumerable:!0}):s;if(o&&typeof o===\"object\"||typeof o===\"function\"){for(let g of G(o))if(!v.call(l,g))F(l,g,{get:H.bind(o,g),enumerable:!0})}if(_)y.set(o,l);return l};var Z=(o,u)=>()=>(u||o((u={exports:{}}).exports,u),u.exports);var S=Z(function(w,U){(function(o){if(typeof w===\"object\")U.exports=o();else if(typeof define===\"function\"&&define.amd)define(o);else{var u;try{u=window}catch(s){u=self}u.SparkMD5=o()}})(function(o){var u=function(i,f){return i+f&4294967295},s=[\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"];function _(i,f,r,t,n,e){return f=u(u(f,i),u(t,e)),u(f<<n|f>>>32-n,r)}function y(i,f){var r=i[0],t=i[1],n=i[2],e=i[3];r+=(t&n|~t&e)+f[0]-680876936|0,r=(r<<7|r>>>25)+t|0,e+=(r&t|~r&n)+f[1]-389564586|0,e=(e<<12|e>>>20)+r|0,n+=(e&r|~e&t)+f[2]+606105819|0,n=(n<<17|n>>>15)+e|0,t+=(n&e|~n&r)+f[3]-1044525330|0,t=(t<<22|t>>>10)+n|0,r+=(t&n|~t&e)+f[4]-176418897|0,r=(r<<7|r>>>25)+t|0,e+=(r&t|~r&n)+f[5]+1200080426|0,e=(e<<12|e>>>20)+r|0,n+=(e&r|~e&t)+f[6]-1473231341|0,n=(n<<17|n>>>15)+e|0,t+=(n&e|~n&r)+f[7]-45705983|0,t=(t<<22|t>>>10)+n|0,r+=(t&n|~t&e)+f[8]+1770035416|0,r=(r<<7|r>>>25)+t|0,e+=(r&t|~r&n)+f[9]-1958414417|0,e=(e<<12|e>>>20)+r|0,n+=(e&r|~e&t)+f[10]-42063|0,n=(n<<17|n>>>15)+e|0,t+=(n&e|~n&r)+f[11]-1990404162|0,t=(t<<22|t>>>10)+n|0,r+=(t&n|~t&e)+f[12]+1804603682|0,r=(r<<7|r>>>25)+t|0,e+=(r&t|~r&n)+f[13]-40341101|0,e=(e<<12|e>>>20)+r|0,n+=(e&r|~e&t)+f[14]-1502002290|0,n=(n<<17|n>>>15)+e|0,t+=(n&e|~n&r)+f[15]+1236535329|0,t=(t<<22|t>>>10)+n|0,r+=(t&e|n&~e)+f[1]-165796510|0,r=(r<<5|r>>>27)+t|0,e+=(r&n|t&~n)+f[6]-1069501632|0,e=(e<<9|e>>>23)+r|0,n+=(e&t|r&~t)+f[11]+643717713|0,n=(n<<14|n>>>18)+e|0,t+=(n&r|e&~r)+f[0]-373897302|0,t=(t<<20|t>>>12)+n|0,r+=(t&e|n&~e)+f[5]-701558691|0,r=(r<<5|r>>>27)+t|0,e+=(r&n|t&~n)+f[10]+38016083|0,e=(e<<9|e>>>23)+r|0,n+=(e&t|r&~t)+f[15]-660478335|0,n=(n<<14|n>>>18)+e|0,t+=(n&r|e&~r)+f[4]-405537848|0,t=(t<<20|t>>>12)+n|0,r+=(t&e|n&~e)+f[9]+568446438|0,r=(r<<5|r>>>27)+t|0,e+=(r&n|t&~n)+f[14]-1019803690|0,e=(e<<9|e>>>23)+r|0,n+=(e&t|r&~t)+f[3]-187363961|0,n=(n<<14|n>>>18)+e|0,t+=(n&r|e&~r)+f[8]+1163531501|0,t=(t<<20|t>>>12)+n|0,r+=(t&e|n&~e)+f[13]-1444681467|0,r=(r<<5|r>>>27)+t|0,e+=(r&n|t&~n)+f[2]-51403784|0,e=(e<<9|e>>>23)+r|0,n+=(e&t|r&~t)+f[7]+1735328473|0,n=(n<<14|n>>>18)+e|0,t+=(n&r|e&~r)+f[12]-1926607734|0,t=(t<<20|t>>>12)+n|0,r+=(t^n^e)+f[5]-378558|0,r=(r<<4|r>>>28)+t|0,e+=(r^t^n)+f[8]-2022574463|0,e=(e<<11|e>>>21)+r|0,n+=(e^r^t)+f[11]+1839030562|0,n=(n<<16|n>>>16)+e|0,t+=(n^e^r)+f[14]-35309556|0,t=(t<<23|t>>>9)+n|0,r+=(t^n^e)+f[1]-1530992060|0,r=(r<<4|r>>>28)+t|0,e+=(r^t^n)+f[4]+1272893353|0,e=(e<<11|e>>>21)+r|0,n+=(e^r^t)+f[7]-155497632|0,n=(n<<16|n>>>16)+e|0,t+=(n^e^r)+f[10]-1094730640|0,t=(t<<23|t>>>9)+n|0,r+=(t^n^e)+f[13]+681279174|0,r=(r<<4|r>>>28)+t|0,e+=(r^t^n)+f[0]-358537222|0,e=(e<<11|e>>>21)+r|0,n+=(e^r^t)+f[3]-722521979|0,n=(n<<16|n>>>16)+e|0,t+=(n^e^r)+f[6]+76029189|0,t=(t<<23|t>>>9)+n|0,r+=(t^n^e)+f[9]-640364487|0,r=(r<<4|r>>>28)+t|0,e+=(r^t^n)+f[12]-421815835|0,e=(e<<11|e>>>21)+r|0,n+=(e^r^t)+f[15]+530742520|0,n=(n<<16|n>>>16)+e|0,t+=(n^e^r)+f[2]-995338651|0,t=(t<<23|t>>>9)+n|0,r+=(n^(t|~e))+f[0]-198630844|0,r=(r<<6|r>>>26)+t|0,e+=(t^(r|~n))+f[7]+1126891415|0,e=(e<<10|e>>>22)+r|0,n+=(r^(e|~t))+f[14]-1416354905|0,n=(n<<15|n>>>17)+e|0,t+=(e^(n|~r))+f[5]-57434055|0,t=(t<<21|t>>>11)+n|0,r+=(n^(t|~e))+f[12]+1700485571|0,r=(r<<6|r>>>26)+t|0,e+=(t^(r|~n))+f[3]-1894986606|0,e=(e<<10|e>>>22)+r|0,n+=(r^(e|~t))+f[10]-1051523|0,n=(n<<15|n>>>17)+e|0,t+=(e^(n|~r))+f[1]-2054922799|0,t=(t<<21|t>>>11)+n|0,r+=(n^(t|~e))+f[8]+1873313359|0,r=(r<<6|r>>>26)+t|0,e+=(t^(r|~n))+f[15]-30611744|0,e=(e<<10|e>>>22)+r|0,n+=(r^(e|~t))+f[6]-1560198380|0,n=(n<<15|n>>>17)+e|0,t+=(e^(n|~r))+f[13]+1309151649|0,t=(t<<21|t>>>11)+n|0,r+=(n^(t|~e))+f[4]-145523070|0,r=(r<<6|r>>>26)+t|0,e+=(t^(r|~n))+f[11]-1120210379|0,e=(e<<10|e>>>22)+r|0,n+=(r^(e|~t))+f[2]+718787259|0,n=(n<<15|n>>>17)+e|0,t+=(e^(n|~r))+f[9]-343485551|0,t=(t<<21|t>>>11)+n|0,i[0]=r+i[0]|0,i[1]=t+i[1]|0,i[2]=n+i[2]|0,i[3]=e+i[3]|0}function b(i){var f=[],r;for(r=0;r<64;r+=4)f[r>>2]=i.charCodeAt(r)+(i.charCodeAt(r+1)<<8)+(i.charCodeAt(r+2)<<16)+(i.charCodeAt(r+3)<<24);return f}function l(i){var f=[],r;for(r=0;r<64;r+=4)f[r>>2]=i[r]+(i[r+1]<<8)+(i[r+2]<<16)+(i[r+3]<<24);return f}function g(i){var f=i.length,r=[1732584193,-271733879,-1732584194,271733878],t,n,e,a,c,p;for(t=64;t<=f;t+=64)y(r,b(i.substring(t-64,t)));i=i.substring(t-64),n=i.length,e=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(t=0;t<n;t+=1)e[t>>2]|=i.charCodeAt(t)<<(t%4<<3);if(e[t>>2]|=128<<(t%4<<3),t>55){y(r,e);for(t=0;t<16;t+=1)e[t]=0}return a=f*8,a=a.toString(16).match(/(.*?)(.{0,8})$/),c=parseInt(a[2],16),p=parseInt(a[1],16)||0,e[14]=c,e[15]=p,y(r,e),r}function T(i){var f=i.length,r=[1732584193,-271733879,-1732584194,271733878],t,n,e,a,c,p;for(t=64;t<=f;t+=64)y(r,l(i.subarray(t-64,t)));i=t-64<f?i.subarray(t-64):new Uint8Array(0),n=i.length,e=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(t=0;t<n;t+=1)e[t>>2]|=i[t]<<(t%4<<3);if(e[t>>2]|=128<<(t%4<<3),t>55){y(r,e);for(t=0;t<16;t+=1)e[t]=0}return a=f*8,a=a.toString(16).match(/(.*?)(.{0,8})$/),c=parseInt(a[2],16),p=parseInt(a[1],16)||0,e[14]=c,e[15]=p,y(r,e),r}function L(i){var f=\"\",r;for(r=0;r<4;r+=1)f+=s[i>>r*8+4&15]+s[i>>r*8&15];return f}function d(i){var f;for(f=0;f<i.length;f+=1)i[f]=L(i[f]);return i.join(\"\")}if(d(g(\"hello\"))!==\"5d41402abc4b2a76b9719d911017c592\")u=function(i,f){var r=(i&65535)+(f&65535),t=(i>>16)+(f>>16)+(r>>16);return t<<16|r&65535};if(typeof ArrayBuffer<\"u\"&&!ArrayBuffer.prototype.slice)(function(){function i(f,r){if(f=f|0||0,f<0)return Math.max(f+r,0);return Math.min(f,r)}ArrayBuffer.prototype.slice=function(f,r){var t=this.byteLength,n=i(f,t),e=t,a,c,p,B;if(r!==o)e=i(r,t);if(n>e)return new ArrayBuffer(0);return a=e-n,c=new ArrayBuffer(a),p=new Uint8Array(c),B=new Uint8Array(this,n,a),p.set(B),c}})();function m(i){if(/[\\u0080-\\uFFFF]/.test(i))i=unescape(encodeURIComponent(i));return i}function M(i,f){var r=i.length,t=new ArrayBuffer(r),n=new Uint8Array(t),e;for(e=0;e<r;e+=1)n[e]=i.charCodeAt(e);return f?n:t}function x(i){return String.fromCharCode.apply(null,new Uint8Array(i))}function j(i,f,r){var t=new Uint8Array(i.byteLength+f.byteLength);return t.set(new Uint8Array(i)),t.set(new Uint8Array(f),i.byteLength),r?t:t.buffer}function A(i){var f=[],r=i.length,t;for(t=0;t<r-1;t+=2)f.push(parseInt(i.substr(t,2),16));return String.fromCharCode.apply(String,f)}function h(){this.reset()}return h.prototype.append=function(i){return this.appendBinary(m(i)),this},h.prototype.appendBinary=function(i){this._buff+=i,this._length+=i.length;var f=this._buff.length,r;for(r=64;r<=f;r+=64)y(this._hash,b(this._buff.substring(r-64,r)));return this._buff=this._buff.substring(r-64),this},h.prototype.end=function(i){var f=this._buff,r=f.length,t,n=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],e;for(t=0;t<r;t+=1)n[t>>2]|=f.charCodeAt(t)<<(t%4<<3);if(this._finish(n,r),e=d(this._hash),i)e=A(e);return this.reset(),e},h.prototype.reset=function(){return this._buff=\"\",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},h.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}},h.prototype.setState=function(i){return this._buff=i.buff,this._length=i.length,this._hash=i.hash,this},h.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},h.prototype._finish=function(i,f){var r=f,t,n,e;if(i[r>>2]|=128<<(r%4<<3),r>55){y(this._hash,i);for(r=0;r<16;r+=1)i[r]=0}t=this._length*8,t=t.toString(16).match(/(.*?)(.{0,8})$/),n=parseInt(t[2],16),e=parseInt(t[1],16)||0,i[14]=n,i[15]=e,y(this._hash,i)},h.hash=function(i,f){return h.hashBinary(m(i),f)},h.hashBinary=function(i,f){var r=g(i),t=d(r);return f?A(t):t},h.ArrayBuffer=function(){this.reset()},h.ArrayBuffer.prototype.append=function(i){var f=j(this._buff.buffer,i,!0),r=f.length,t;this._length+=i.byteLength;for(t=64;t<=r;t+=64)y(this._hash,l(f.subarray(t-64,t)));return this._buff=t-64<r?new Uint8Array(f.buffer.slice(t-64)):new Uint8Array(0),this},h.ArrayBuffer.prototype.end=function(i){var f=this._buff,r=f.length,t=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n,e;for(n=0;n<r;n+=1)t[n>>2]|=f[n]<<(n%4<<3);if(this._finish(t,r),e=d(this._hash),i)e=A(e);return this.reset(),e},h.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},h.ArrayBuffer.prototype.getState=function(){var i=h.prototype.getState.call(this);return i.buff=x(i.buff),i},h.ArrayBuffer.prototype.setState=function(i){return i.buff=M(i.buff,!0),h.prototype.setState.call(this,i)},h.ArrayBuffer.prototype.destroy=h.prototype.destroy,h.ArrayBuffer.prototype._finish=h.prototype._finish,h.ArrayBuffer.hash=function(i,f){var r=T(new Uint8Array(i)),t=d(r);return f?A(t):t},h})});var I=R(S(),1);function*C(o,u){for(let s=0;s<o.length;s+=u)yield o.slice(s,s+u)}var q=8192;self.onmessage=({data:o})=>{if(o.event===\"md5\"){let u=new I.default.ArrayBuffer;for(let s of C(o.data,q))u.append(s.buffer);postMessage({event:\"md5-result\",data:u.end()})}};})();\n";