@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.
- package/dist/lib/client/SecrecyCloudClient.js +6 -5
- package/dist/lib/client/data-link.js +11 -6
- package/dist/lib/client/helpers.js +15 -1
- package/dist/lib/client/upload.js +9 -9
- package/dist/lib/crypto/data.js +7 -4
- package/dist/lib/crypto/domain.js +3 -1
- package/dist/lib/crypto/helpers.js +10 -2
- package/dist/lib/crypto/index.js +9 -2
- package/dist/lib/versioning.js +1 -1
- package/dist/lib/worker/generated/sodium.js +1 -1
- package/dist/lib/worker/sodium.js +4 -3
- package/dist/types/client/data-link.d.ts +4 -4
- package/dist/types/client/helpers.d.ts +1 -0
- package/dist/types/client/types/data.d.ts +1 -2
- package/dist/types/client.d.ts +74 -66
- package/dist/types/crypto/data.d.ts +13 -3
- package/dist/types/crypto/domain.d.ts +1 -0
- package/dist/types/crypto/helpers.d.ts +6 -2
- package/dist/types/crypto/index.d.ts +9 -3
- package/dist/types/sodium.d.ts +0 -5
- package/dist/types/versioning.d.ts +1 -1
- package/dist/types/worker/generated/sodium.d.ts +1 -1
- package/dist/types/worker/sodium.d.ts +2 -2
- package/package.json +7 -6
|
@@ -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
|
-
?
|
|
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
|
-
|
|
706
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
258
|
+
md5: uploadDataArgs.md5,
|
|
259
259
|
size: uploadDataArgs.size,
|
|
260
260
|
sizeEncrypted: uploadDataArgs.type === 'encrypted'
|
|
261
261
|
? uploadDataArgs.sizeEncrypted
|
package/dist/lib/crypto/data.js
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
21
|
-
|
|
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);
|
package/dist/lib/crypto/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
package/dist/lib/versioning.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SECRECY_LIB_VERSION = '
|
|
1
|
+
export const SECRECY_LIB_VERSION = '1.86.4-fix-crypto.1';
|