@secrecy/lib 1.86.2-fix-crypto.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.
@@ -700,13 +700,12 @@ export class SecrecyCloudClient {
700
700
  : this.#client.currentIdentity.identityPubKey, this.#client.currentIdentityPrivateKey)
701
701
  : null;
702
702
  const src = key
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
- })
703
+ ? await decrypt(key, encryptedContent, progressDecrypt, signal)
707
704
  : encryptedContent;
708
- const md5Content = await md5(src);
709
- 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) {
710
709
  throw new Error(`Content does not match`);
711
710
  }
712
711
  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';
7
6
  import { decompress } from '../minify';
8
7
  const downloadDataLinkS3Schema = z.object({
9
8
  name: z.string(),
10
- md5: z.string(),
9
+ // Only for unencrypted and legacy encrypted data.
10
+ md5: z.string().nullable(),
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,7 +16,8 @@ const downloadDataLinkS3Schema = z.object({
16
16
  });
17
17
  const downloadDataLinkLiteSchema = z.object({
18
18
  name: z.string(),
19
- md5: z.string(),
19
+ // Only for unencrypted and legacy encrypted data.
20
+ md5: z.string().nullable(),
20
21
  md5Encrypted: z.string().nullable(),
21
22
  size: z.coerce.bigint().gte(0n),
22
23
  bytes: z.string(),
@@ -68,14 +69,8 @@ export async function downloadDataFromLink(opts) {
68
69
  throw new Error('Unable to read encrypted data without password!');
69
70
  }
70
71
  try {
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
- }
72
+ const decryptedSharingDataKey = decryptKeyWithPassword(sodium.from_hex(opts.crypto.key), opts.crypto.password);
73
+ const decryptedBytes = await decrypt(decryptedSharingDataKey, bytes, opts.decryptProgress, opts.signal);
79
74
  return decompress(decryptedBytes);
80
75
  }
81
76
  catch (err) {
@@ -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?.({
@@ -100,14 +102,13 @@ export async function uploadData({ storageType, data, password, forcePassword =
100
102
  size: BigInt(dataBuffer.byteLength),
101
103
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
102
104
  md5Encrypted,
103
- md5: md5Data,
104
105
  fromIdentityPubKey: currentIdentity?.identityPubKey,
105
106
  ...filetype,
106
107
  }
107
108
  : {
108
109
  type: 'unencrypted',
109
110
  content: Buffer.from(encryptedData),
110
- md5: md5Data,
111
+ md5: await md5(compressed),
111
112
  size: BigInt(dataBuffer.byteLength),
112
113
  fromIdentityPubKey: currentIdentity?.identityPubKey,
113
114
  ...filetype,
@@ -122,7 +123,7 @@ export async function uploadData({ storageType, data, password, forcePassword =
122
123
  const localData = {
123
124
  id: uploadData.id,
124
125
  storageType: 'lite',
125
- md5: uploadDataArgs.md5,
126
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
126
127
  size: uploadDataArgs.size,
127
128
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
128
129
  ? uploadDataArgs.sizeEncrypted
@@ -144,13 +145,12 @@ export async function uploadData({ storageType, data, password, forcePassword =
144
145
  size: BigInt(dataBuffer.byteLength),
145
146
  key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
146
147
  md5Encrypted,
147
- md5: md5Data,
148
148
  fromIdentityPubKey: currentIdentity?.identityPubKey,
149
149
  ...filetype,
150
150
  }
151
151
  : {
152
152
  type: 'unencrypted',
153
- md5: md5Data,
153
+ md5: await md5(compressed),
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.md5,
174
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
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.md5,
258
+ md5: uploadDataArgs.type === 'unencrypted' ? uploadDataArgs.md5 : null,
259
259
  size: uploadDataArgs.size,
260
260
  sizeEncrypted: uploadDataArgs.type === 'encrypted'
261
261
  ? uploadDataArgs.sizeEncrypted
@@ -79,7 +79,6 @@ 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();
83
82
  final.set(header);
84
83
  sparkEncrypted.append(header.buffer);
85
84
  let total = header.byteLength;
@@ -93,8 +92,7 @@ export async function encryptSecretStream(key, data, progress, signal) {
93
92
  if (signal?.aborted === true) {
94
93
  throw new Error(`Encrypt aborted`);
95
94
  }
96
- const chunk = data.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
97
- spark.append(chunk.buffer);
95
+ const chunk = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
98
96
  const tag = i === chunkCount - 1
99
97
  ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
100
98
  : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
@@ -120,7 +118,6 @@ export async function encryptSecretStream(key, data, progress, signal) {
120
118
  return {
121
119
  data: final,
122
120
  md5Encrypted: sparkEncrypted.end(),
123
- md5: spark.end(),
124
121
  };
125
122
  }
126
123
  finally {
@@ -130,9 +127,9 @@ export async function encryptSecretStream(key, data, progress, signal) {
130
127
  /**
131
128
  * Decrypt a secretstream produced by `encryptSecretStream`. Throws if any
132
129
  * chunk fails authentication, if data follows the `TAG_FINAL` chunk, or if the
133
- * stream ends without it (truncation) unless `allowMissingFinalTag` is set.
130
+ * stream ends without it (truncation).
134
131
  */
135
- export async function decryptSecretStream(key, data, progress, signal, options) {
132
+ export async function decryptSecretStream(key, data, progress, signal) {
136
133
  await setup();
137
134
  const headerBytes = sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES;
138
135
  const chunkSize = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
@@ -189,7 +186,7 @@ export async function decryptSecretStream(key, data, progress, signal, options)
189
186
  lastPercent = percent;
190
187
  }
191
188
  }
192
- if (!finalTagSeen && options?.allowMissingFinalTag !== true) {
189
+ if (!finalTagSeen) {
193
190
  throw new Error('Unable to decrypt the data: the data is truncated!');
194
191
  }
195
192
  void progress?.({
@@ -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
  }
@@ -17,19 +17,11 @@ 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
- /**
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) {
20
+ /** Unwrap a data key produced by `encryptKeyWithPassword`. */
21
+ export function decryptKeyWithPassword(wrapped, password) {
26
22
  const boxBytes = sodium.crypto_secretbox_NONCEBYTES +
27
23
  sodium.crypto_secretbox_MACBYTES +
28
24
  sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES;
29
- if (wrapped.byteLength === boxBytes) {
30
- const salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, password + legacyMd5);
31
- return decryptSecretBox(wrapped, derivePassword(password, salt));
32
- }
33
25
  if (wrapped.byteLength === 1 + sodium.crypto_pwhash_SALTBYTES + boxBytes &&
34
26
  wrapped[0] === PASSWORD_KEY_VERSION) {
35
27
  const salt = wrapped.slice(1, 1 + sodium.crypto_pwhash_SALTBYTES);