@secrecy/lib 1.86.4-fix-md5.1 → 1.86.4-fix-crypto.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.
@@ -700,12 +700,13 @@ export class SecrecyCloudClient {
700
700
  : this.#client.currentIdentity.identityPubKey, this.#client.currentIdentityPrivateKey)
701
701
  : null;
702
702
  const src = key
703
- ? await decrypt(key, encryptedContent, progressDecrypt, signal)
703
+ ? // Legacy streams may lack the final tag: the md5 check below covers them.
704
+ await decrypt(key, encryptedContent, progressDecrypt, signal, {
705
+ allowMissingFinalTag: true,
706
+ })
704
707
  : encryptedContent;
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) {
708
+ const md5Content = await md5(src);
709
+ if (md5Content !== dataContent.md5) {
709
710
  throw new Error(`Content does not match`);
710
711
  }
711
712
  const decompressed = decompress(src);
@@ -3,11 +3,11 @@ import { buildBytesFromDataLink } from '../crypto/domain';
3
3
  import { sodium } from '../sodium';
4
4
  import { decryptKeyWithPassword } from '../crypto/helpers';
5
5
  import { decrypt } from '../worker/sodium';
6
+ import { md5 } from '../worker/md5';
6
7
  import { decompress } from '../minify';
7
8
  const downloadDataLinkS3Schema = z.object({
8
9
  name: z.string(),
9
- // Only for unencrypted and legacy encrypted data.
10
- md5: z.string().nullable(),
10
+ md5: z.string(),
11
11
  md5Encrypted: z.string().nullable(),
12
12
  size: z.coerce.bigint().gte(0n),
13
13
  parts: z.array(z.object({ order: z.number(), md5: z.string(), contentUrl: z.string() })),
@@ -16,8 +16,7 @@ const downloadDataLinkS3Schema = z.object({
16
16
  });
17
17
  const downloadDataLinkLiteSchema = z.object({
18
18
  name: z.string(),
19
- // Only for unencrypted and legacy encrypted data.
20
- md5: z.string().nullable(),
19
+ md5: z.string(),
21
20
  md5Encrypted: z.string().nullable(),
22
21
  size: z.coerce.bigint().gte(0n),
23
22
  bytes: z.string(),
@@ -69,8 +68,14 @@ export async function downloadDataFromLink(opts) {
69
68
  throw new Error('Unable to read encrypted data without password!');
70
69
  }
71
70
  try {
72
- const decryptedSharingDataKey = decryptKeyWithPassword(sodium.from_hex(opts.crypto.key), opts.crypto.password);
73
- const decryptedBytes = await decrypt(decryptedSharingDataKey, bytes, opts.decryptProgress, opts.signal);
71
+ const decryptedSharingDataKey = decryptKeyWithPassword(sodium.from_hex(opts.crypto.key), opts.crypto.password, dataLink.md5);
72
+ const decryptedBytes = await decrypt(decryptedSharingDataKey, bytes, opts.decryptProgress, opts.signal,
73
+ // Legacy streams may lack the final tag: the md5 check below covers them.
74
+ { allowMissingFinalTag: true });
75
+ const md5Content = await md5(decryptedBytes);
76
+ if (md5Content !== dataLink.md5) {
77
+ throw new Error(`Content does not match`);
78
+ }
74
79
  return decompress(decryptedBytes);
75
80
  }
76
81
  catch (err) {
@@ -3,11 +3,22 @@ import { popup } from '../utils/popup-tools.js';
3
3
  import { secrecyUserApp } from './types/index.js';
4
4
  import { getStorage } from './storage.js';
5
5
  import { setup } from '../sodium.js';
6
+ export function assertBrowser(fn) {
7
+ if (typeof window === 'undefined') {
8
+ const err = {
9
+ name: 'ClientError',
10
+ code: 'SERVER_ENVIRONMENT',
11
+ message: `${fn}() can only run in a browser. On Next.js, call it from a "use client" component, or via dynamic(..., { ssr: false }).`,
12
+ };
13
+ throw err;
14
+ }
15
+ }
6
16
  export function parseInfos() {
17
+ assertBrowser('parseInfos');
7
18
  if (window.location.hash === '') {
8
19
  return null;
9
20
  }
10
- const hash = window.location.hash.substr(1);
21
+ const hash = window.location.hash.slice(1);
11
22
  try {
12
23
  const res = JSON.parse(atob(hash));
13
24
  // Replace (not push) the entry: the fragment holds the private keys and
@@ -21,6 +32,7 @@ export function parseInfos() {
21
32
  }
22
33
  }
23
34
  export function getSecrecyClient(opts = {}) {
35
+ assertBrowser('getSecrecyClient');
24
36
  const storage = getStorage(opts.session);
25
37
  const infos = parseInfos();
26
38
  if (infos !== null) {
@@ -52,6 +64,7 @@ export function getSecrecyClient(opts = {}) {
52
64
  return null;
53
65
  }
54
66
  export async function login({ appId, context, path, redirect, scopes, backPath, session, secrecyUrls, forceLogin, }) {
67
+ assertBrowser('login');
55
68
  return await new Promise(async (resolve, reject) => {
56
69
  const appUrl = window.location.origin;
57
70
  const client = getSecrecyClient({ session, secrecyUrls });
@@ -139,6 +152,7 @@ export async function login({ appId, context, path, redirect, scopes, backPath,
139
152
  * juggle the sync/async mismatch between them.
140
153
  */
141
154
  export async function initSecrecy(opts) {
155
+ assertBrowser('initSecrecy');
142
156
  await setup();
143
157
  const existing = getSecrecyClient({
144
158
  session: opts.session,
@@ -40,10 +40,7 @@ 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
- // 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
43
+ let { encryptedData, encryptedDataKey, dataKey, md5Data, md5Encrypted } = encrypted
47
44
  ? await encryptDataAndKey({
48
45
  data: compressed,
49
46
  progress: encryptProgress,
@@ -55,6 +52,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
55
52
  encryptedData: compressed,
56
53
  encryptedDataKey: null,
57
54
  dataKey: null,
55
+ md5Data: await md5(compressed),
58
56
  md5Encrypted: null,
59
57
  };
60
58
  await uploadProgress?.({
@@ -102,13 +100,14 @@ export async function uploadData({ storageType, data, password, forcePassword =
102
100
  size: BigInt(dataBuffer.byteLength),
103
101
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
104
102
  md5Encrypted,
103
+ md5: md5Data,
105
104
  fromIdentityPubKey: currentIdentity?.identityPubKey,
106
105
  ...filetype,
107
106
  }
108
107
  : {
109
108
  type: 'unencrypted',
110
109
  content: Buffer.from(encryptedData),
111
- md5: await md5(compressed),
110
+ md5: md5Data,
112
111
  size: BigInt(dataBuffer.byteLength),
113
112
  fromIdentityPubKey: currentIdentity?.identityPubKey,
114
113
  ...filetype,
@@ -123,7 +122,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
123
122
  const localData = {
124
123
  id: uploadData.id,
125
124
  storageType: 'lite',
126
- md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
125
+ md5: uploadDataArgs.md5,
127
126
  size: uploadDataArgs.size,
128
127
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
129
128
  ? uploadDataArgs.sizeEncrypted
@@ -145,12 +144,13 @@ export async function uploadData({ storageType, data, password, forcePassword =
145
144
  size: BigInt(dataBuffer.byteLength),
146
145
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
147
146
  md5Encrypted,
147
+ md5: md5Data,
148
148
  fromIdentityPubKey: currentIdentity?.identityPubKey,
149
149
  ...filetype,
150
150
  }
151
151
  : {
152
152
  type: 'unencrypted',
153
- md5: await md5(compressed),
153
+ md5: md5Data,
154
154
  size: BigInt(dataBuffer.byteLength),
155
155
  fromIdentityPubKey: currentIdentity?.identityPubKey,
156
156
  ...filetype,
@@ -171,7 +171,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
171
171
  const localData = {
172
172
  id: uploadData.id,
173
173
  storageType: storageType,
174
- md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
174
+ md5: uploadDataArgs.md5,
175
175
  size: uploadDataArgs.size,
176
176
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
177
177
  ? uploadDataArgs.sizeEncrypted
@@ -255,7 +255,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
255
255
  const localData = {
256
256
  id: uploadData.id,
257
257
  storageType: storageType,
258
- md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
258
+ md5: uploadDataArgs.md5,
259
259
  size: uploadDataArgs.size,
260
260
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
261
261
  ? uploadDataArgs.sizeEncrypted
@@ -79,6 +79,7 @@ export async function encryptSecretStream(key, data, progress, signal) {
79
79
  });
80
80
  const final = new Uint8Array(max);
81
81
  const sparkEncrypted = new SparkMD5.ArrayBuffer();
82
+ const spark = new SparkMD5.ArrayBuffer();
82
83
  final.set(header);
83
84
  sparkEncrypted.append(header.buffer);
84
85
  let total = header.byteLength;
@@ -92,7 +93,8 @@ export async function encryptSecretStream(key, data, progress, signal) {
92
93
  if (signal?.aborted === true) {
93
94
  throw new Error(`Encrypt aborted`);
94
95
  }
95
- const chunk = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
96
+ const chunk = data.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
97
+ spark.append(chunk.buffer);
96
98
  const tag = i === chunkCount - 1
97
99
  ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
98
100
  : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
@@ -118,6 +120,7 @@ export async function encryptSecretStream(key, data, progress, signal) {
118
120
  return {
119
121
  data: final,
120
122
  md5Encrypted: sparkEncrypted.end(),
123
+ md5: spark.end(),
121
124
  };
122
125
  }
123
126
  finally {
@@ -127,9 +130,9 @@ export async function encryptSecretStream(key, data, progress, signal) {
127
130
  /**
128
131
  * Decrypt a secretstream produced by `encryptSecretStream`. Throws if any
129
132
  * chunk fails authentication, if data follows the `TAG_FINAL` chunk, or if the
130
- * stream ends without it (truncation).
133
+ * stream ends without it (truncation) unless `allowMissingFinalTag` is set.
131
134
  */
132
- export async function decryptSecretStream(key, data, progress, signal) {
135
+ export async function decryptSecretStream(key, data, progress, signal, options) {
133
136
  await setup();
134
137
  const headerBytes = sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES;
135
138
  const chunkSize = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
@@ -186,7 +189,7 @@ export async function decryptSecretStream(key, data, progress, signal) {
186
189
  lastPercent = percent;
187
190
  }
188
191
  }
189
- if (!finalTagSeen) {
192
+ if (!finalTagSeen && options?.allowMissingFinalTag !== true) {
190
193
  throw new Error('Unable to decrypt the data: the data is truncated!');
191
194
  }
192
195
  void progress?.({
@@ -11,11 +11,12 @@ 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, md5Encrypted } = await encrypt(dataKey, data, progress, signal);
14
+ const { data: encryptedData, md5: md5Data, md5Encrypted, } = await encrypt(dataKey, data, progress, signal);
15
15
  if (!currentIdentity || !keyPairs) {
16
16
  return {
17
17
  encryptedData,
18
18
  dataKey,
19
+ md5Data,
19
20
  md5Encrypted,
20
21
  };
21
22
  }
@@ -28,6 +29,7 @@ export async function encryptDataAndKey({ data, currentIdentity, keyPairs, progr
28
29
  encryptedDataKey: encDataKey,
29
30
  encryptedData,
30
31
  dataKey,
32
+ md5Data,
31
33
  md5Encrypted,
32
34
  };
33
35
  }
@@ -17,11 +17,19 @@ export function encryptKeyWithPassword(key, password) {
17
17
  const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
18
18
  return concatenate(new Uint8Array([PASSWORD_KEY_VERSION]), salt, encryptSecretBox(key, derivePassword(password, salt)));
19
19
  }
20
- /** Unwrap a data key produced by `encryptKeyWithPassword`. */
21
- export function decryptKeyWithPassword(wrapped, password) {
20
+ /**
21
+ * Unwrap a data key produced by `encryptKeyWithPassword`. Also reads the
22
+ * legacy format (`nonce || secretbox(key)`) whose salt was derived from the
23
+ * password and the plaintext md5, hence `legacyMd5`.
24
+ */
25
+ export function decryptKeyWithPassword(wrapped, password, legacyMd5) {
22
26
  const boxBytes = sodium.crypto_secretbox_NONCEBYTES +
23
27
  sodium.crypto_secretbox_MACBYTES +
24
28
  sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES;
29
+ if (wrapped.byteLength === boxBytes) {
30
+ const salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, password + legacyMd5, null);
31
+ return decryptSecretBox(wrapped, derivePassword(password, salt));
32
+ }
25
33
  if (wrapped.byteLength === 1 + sodium.crypto_pwhash_SALTBYTES + boxBytes &&
26
34
  wrapped[0] === PASSWORD_KEY_VERSION) {
27
35
  const salt = wrapped.slice(1, 1 + sodium.crypto_pwhash_SALTBYTES);
@@ -1,5 +1,12 @@
1
1
  import { concatenate } from '../utils/array.js';
2
2
  import { sodium } from '../sodium.js';
3
+ function dataTooShortError() {
4
+ return {
5
+ name: 'ClientError',
6
+ code: 'DATA_TOO_SHORT',
7
+ message: 'Encrypted payload is shorter than nonce + MAC',
8
+ };
9
+ }
3
10
  export function encryptCryptoBox(data, publicKeyBob, privateKeyAlice) {
4
11
  const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
5
12
  const crypt = sodium.crypto_box_easy(data, nonce, sodium.from_hex(publicKeyBob), sodium.from_hex(privateKeyAlice));
@@ -10,7 +17,7 @@ export function generateCryptoBoxKeyPair() {
10
17
  }
11
18
  export function decryptCryptoBox(data, publicKeyAlice, privateKeyBob) {
12
19
  if (data.length < sodium.crypto_box_NONCEBYTES + sodium.crypto_box_MACBYTES) {
13
- throw new Error('data too short');
20
+ throw dataTooShortError();
14
21
  }
15
22
  const nonce = data.slice(0, sodium.crypto_box_NONCEBYTES);
16
23
  const cipher = data.slice(sodium.crypto_box_NONCEBYTES);
@@ -27,7 +34,7 @@ export function encryptSecretBox(data, key) {
27
34
  export function decryptSecretBox(data, key) {
28
35
  if (data.length <
29
36
  sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) {
30
- throw new Error('data too short');
37
+ throw dataTooShortError();
31
38
  }
32
39
  const nonce = data.slice(0, sodium.crypto_secretbox_NONCEBYTES);
33
40
  const cipher = data.slice(sodium.crypto_secretbox_NONCEBYTES);
@@ -1 +1 @@
1
- export const SECRECY_LIB_VERSION = '0.0.0-development';
1
+ export const SECRECY_LIB_VERSION = '1.86.4-fix-crypto.1';