@secrecy/lib 1.86.3 → 1.86.4-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.
- package/dist/lib/client/SecrecyCloudClient.js +4 -2
- package/dist/lib/client/data-link.js +6 -12
- package/dist/lib/client/helpers.js +4 -16
- package/dist/lib/client/upload.js +12 -14
- package/dist/lib/crypto/data.js +146 -91
- package/dist/lib/crypto/domain.js +1 -3
- package/dist/lib/crypto/helpers.js +23 -1
- package/dist/lib/crypto/index.js +2 -9
- package/dist/lib/utils/popup-tools.js +18 -8
- package/dist/lib/versioning.js +1 -1
- package/dist/lib/worker/generated/md5.js +2 -0
- package/dist/lib/worker/generated/sodium.js +2 -0
- package/dist/lib/worker/md5.js +2 -2
- package/dist/lib/worker/sodium.js +17 -15
- package/dist/types/client/data-link.d.ts +4 -4
- package/dist/types/client/helpers.d.ts +0 -1
- package/dist/types/client/types/data.d.ts +2 -1
- package/dist/types/client.d.ts +66 -74
- package/dist/types/crypto/data.d.ts +11 -1
- package/dist/types/crypto/domain.d.ts +0 -1
- package/dist/types/crypto/helpers.d.ts +7 -0
- package/dist/types/crypto/index.d.ts +3 -3
- package/dist/types/sodium.d.ts +5 -0
- package/dist/types/utils/popup-tools.d.ts +4 -0
- package/dist/types/versioning.d.ts +1 -1
- package/dist/types/worker/generated/md5.d.ts +1 -0
- package/dist/types/worker/generated/sodium.d.ts +1 -0
- package/package.json +8 -8
- package/dist/lib/worker/workerCodes.js +0 -254
- package/dist/types/worker/workerCodes.d.ts +0 -2
|
@@ -702,8 +702,10 @@ export class SecrecyCloudClient {
|
|
|
702
702
|
const src = key
|
|
703
703
|
? await decrypt(key, encryptedContent, progressDecrypt, signal)
|
|
704
704
|
: encryptedContent;
|
|
705
|
-
|
|
706
|
-
if (
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|
|
@@ -3,25 +3,16 @@ 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
|
-
}
|
|
16
6
|
export function parseInfos() {
|
|
17
|
-
assertBrowser('parseInfos');
|
|
18
7
|
if (window.location.hash === '') {
|
|
19
8
|
return null;
|
|
20
9
|
}
|
|
21
|
-
const hash = window.location.hash.
|
|
10
|
+
const hash = window.location.hash.substr(1);
|
|
22
11
|
try {
|
|
23
12
|
const res = JSON.parse(atob(hash));
|
|
24
|
-
|
|
13
|
+
// Replace (not push) the entry: the fragment holds the private keys and
|
|
14
|
+
// must not stay in the browser history.
|
|
15
|
+
window.history.replaceState(window.history.state, '', window.location.pathname + window.location.search);
|
|
25
16
|
const data = secrecyUserApp.safeParse(res);
|
|
26
17
|
return data.success ? data.data : null;
|
|
27
18
|
}
|
|
@@ -30,7 +21,6 @@ export function parseInfos() {
|
|
|
30
21
|
}
|
|
31
22
|
}
|
|
32
23
|
export function getSecrecyClient(opts = {}) {
|
|
33
|
-
assertBrowser('getSecrecyClient');
|
|
34
24
|
const storage = getStorage(opts.session);
|
|
35
25
|
const infos = parseInfos();
|
|
36
26
|
if (infos !== null) {
|
|
@@ -62,7 +52,6 @@ export function getSecrecyClient(opts = {}) {
|
|
|
62
52
|
return null;
|
|
63
53
|
}
|
|
64
54
|
export async function login({ appId, context, path, redirect, scopes, backPath, session, secrecyUrls, forceLogin, }) {
|
|
65
|
-
assertBrowser('login');
|
|
66
55
|
return await new Promise(async (resolve, reject) => {
|
|
67
56
|
const appUrl = window.location.origin;
|
|
68
57
|
const client = getSecrecyClient({ session, secrecyUrls });
|
|
@@ -150,7 +139,6 @@ export async function login({ appId, context, path, redirect, scopes, backPath,
|
|
|
150
139
|
* juggle the sync/async mismatch between them.
|
|
151
140
|
*/
|
|
152
141
|
export async function initSecrecy(opts) {
|
|
153
|
-
assertBrowser('initSecrecy');
|
|
154
142
|
await setup();
|
|
155
143
|
const existing = getSecrecyClient({
|
|
156
144
|
session: opts.session,
|
|
@@ -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 {
|
|
12
|
-
import { decryptCryptoBox
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
package/dist/lib/crypto/data.js
CHANGED
|
@@ -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,18 +39,19 @@ 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
|
|
32
|
-
|
|
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) => {
|
|
36
48
|
assert(!destroyed, 'state already destroyed');
|
|
37
|
-
return sodium.crypto_secretstream_xchacha20poly1305_pull(state, ciphertext);
|
|
49
|
+
return sodium.crypto_secretstream_xchacha20poly1305_pull(state, ciphertext, null);
|
|
38
50
|
};
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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,
|
|
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);
|
package/dist/lib/crypto/index.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
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
|
-
}
|
|
10
3
|
export function encryptCryptoBox(data, publicKeyBob, privateKeyAlice) {
|
|
11
4
|
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
12
5
|
const crypt = sodium.crypto_box_easy(data, nonce, sodium.from_hex(publicKeyBob), sodium.from_hex(privateKeyAlice));
|
|
@@ -17,7 +10,7 @@ export function generateCryptoBoxKeyPair() {
|
|
|
17
10
|
}
|
|
18
11
|
export function decryptCryptoBox(data, publicKeyAlice, privateKeyBob) {
|
|
19
12
|
if (data.length < sodium.crypto_box_NONCEBYTES + sodium.crypto_box_MACBYTES) {
|
|
20
|
-
throw
|
|
13
|
+
throw new Error('data too short');
|
|
21
14
|
}
|
|
22
15
|
const nonce = data.slice(0, sodium.crypto_box_NONCEBYTES);
|
|
23
16
|
const cipher = data.slice(sodium.crypto_box_NONCEBYTES);
|
|
@@ -34,7 +27,7 @@ export function encryptSecretBox(data, key) {
|
|
|
34
27
|
export function decryptSecretBox(data, key) {
|
|
35
28
|
if (data.length <
|
|
36
29
|
sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) {
|
|
37
|
-
throw
|
|
30
|
+
throw new Error('data too short');
|
|
38
31
|
}
|
|
39
32
|
const nonce = data.slice(0, sodium.crypto_secretbox_NONCEBYTES);
|
|
40
33
|
const cipher = data.slice(sodium.crypto_secretbox_NONCEBYTES);
|
|
@@ -98,26 +98,30 @@ function popupExecute(execute, url, name, options, callback) {
|
|
|
98
98
|
//
|
|
99
99
|
};
|
|
100
100
|
const optionsString = optionsToString(popupOptions);
|
|
101
|
-
|
|
101
|
+
// Only the page we opened may answer: the popup name is predictable, so
|
|
102
|
+
// without these checks any window could inject a response (e.g. an
|
|
103
|
+
// attacker's session and keys on login).
|
|
102
104
|
const expectedOrigin = new URL(url, window.location.href).origin;
|
|
105
|
+
const win = execute(url, popupName, optionsString);
|
|
103
106
|
let isMessageSent = false;
|
|
104
107
|
let interval;
|
|
105
108
|
function popupCallbackOnce(err, data) {
|
|
106
109
|
if (!isMessageSent) {
|
|
107
110
|
isMessageSent = true;
|
|
111
|
+
window.removeEventListener('message', onMessage);
|
|
112
|
+
clearInterval(interval);
|
|
108
113
|
popupCallback(err, data);
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
function onMessage(message) {
|
|
112
|
-
if (
|
|
113
|
-
|
|
117
|
+
if (win === null ||
|
|
118
|
+
message.source !== win ||
|
|
119
|
+
message.origin !== expectedOrigin) {
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
const data = message.data;
|
|
117
|
-
if (data !== undefined && data.id === popupName) {
|
|
123
|
+
if (data !== undefined && data !== null && data.id === popupName) {
|
|
118
124
|
popupCallbackOnce(undefined, data.data);
|
|
119
|
-
window.removeEventListener('message', onMessage);
|
|
120
|
-
clearInterval(interval);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
window.addEventListener('message', onMessage, false);
|
|
@@ -181,8 +185,14 @@ export function popupWithPost(url, postData, name, options, callback) {
|
|
|
181
185
|
}
|
|
182
186
|
/**
|
|
183
187
|
* Return html that when executed, will trigger the popup to callback with a response
|
|
188
|
+
*
|
|
189
|
+
* @param {Object}
|
|
190
|
+
* @param {String} targetOrigin origin of the opener allowed to receive `data`
|
|
191
|
+
* @return {String}
|
|
184
192
|
*/
|
|
185
193
|
export function popupResponse(id, data, targetOrigin) {
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
// Escape "<" so the payload cannot close the script tag.
|
|
195
|
+
const jsonData = JSON.stringify({ id, data }).replaceAll('<', '\\u003c');
|
|
196
|
+
const origin = JSON.stringify(targetOrigin).replaceAll('<', '\\u003c');
|
|
197
|
+
return `<script>window.opener.postMessage(${jsonData}, ${origin});setTimeout(function() { window.close(); }, 50);</script>`;
|
|
188
198
|
}
|
package/dist/lib/versioning.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SECRECY_LIB_VERSION = '
|
|
1
|
+
export const SECRECY_LIB_VERSION = '0.0.0-development';
|