@secrecy/lib 1.74.0-feat-groups-identity.4 → 1.74.0-feat-transfer-adaptations.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.
Files changed (54) hide show
  1. package/dist/lib/base-client.js +4 -27
  2. package/dist/lib/client/SecrecyAppClient.js +17 -13
  3. package/dist/lib/client/SecrecyCloudClient.js +156 -368
  4. package/dist/lib/client/SecrecyDbClient.js +7 -3
  5. package/dist/lib/client/SecrecyMailClient.js +48 -38
  6. package/dist/lib/client/SecrecyOrganizationClient.js +12 -10
  7. package/dist/lib/client/SecrecyPayClient.js +5 -1
  8. package/dist/lib/client/SecrecyPseudonymClient.js +8 -4
  9. package/dist/lib/client/SecrecyUserClient.js +11 -11
  10. package/dist/lib/client/SecrecyWalletClient.js +2 -0
  11. package/dist/lib/client/convert/data.js +4 -4
  12. package/dist/lib/client/convert/mail.js +6 -5
  13. package/dist/lib/client/convert/node.js +34 -46
  14. package/dist/lib/client/data-link.js +77 -0
  15. package/dist/lib/client/download.js +84 -0
  16. package/dist/lib/client/helpers.js +11 -18
  17. package/dist/lib/client/index.js +12 -48
  18. package/dist/lib/client/storage.js +2 -3
  19. package/dist/lib/client/types/index.js +7 -3
  20. package/dist/lib/client/upload.js +252 -0
  21. package/dist/lib/client.js +7 -0
  22. package/dist/lib/crypto/data.js +3 -0
  23. package/dist/lib/crypto/domain.js +123 -12
  24. package/dist/lib/crypto/helpers.js +23 -0
  25. package/dist/lib/index.js +0 -1
  26. package/dist/types/base-client.d.ts +1 -2
  27. package/dist/types/client/SecrecyAppClient.d.ts +3 -2
  28. package/dist/types/client/SecrecyCloudClient.d.ts +28 -20
  29. package/dist/types/client/SecrecyDbClient.d.ts +3 -1
  30. package/dist/types/client/SecrecyMailClient.d.ts +3 -2
  31. package/dist/types/client/SecrecyOrganizationClient.d.ts +3 -2
  32. package/dist/types/client/SecrecyPayClient.d.ts +3 -1
  33. package/dist/types/client/SecrecyPseudonymClient.d.ts +3 -2
  34. package/dist/types/client/SecrecyUserClient.d.ts +3 -2
  35. package/dist/types/client/convert/data.d.ts +3 -3
  36. package/dist/types/client/convert/mail.d.ts +5 -3
  37. package/dist/types/client/convert/node.d.ts +5 -5
  38. package/dist/types/client/data-link.d.ts +37 -0
  39. package/dist/types/client/download.d.ts +2 -0
  40. package/dist/types/client/index.d.ts +3 -11
  41. package/dist/types/client/storage.d.ts +2 -3
  42. package/dist/types/client/types/index.d.ts +9 -13
  43. package/dist/types/client/types/mail.d.ts +1 -2
  44. package/dist/types/client/types/node.d.ts +9 -12
  45. package/dist/types/client/types/user.d.ts +0 -15
  46. package/dist/types/client/upload.d.ts +38 -0
  47. package/dist/types/client.d.ts +6174 -681
  48. package/dist/types/crypto/domain.d.ts +36 -8
  49. package/dist/types/crypto/helpers.d.ts +12 -0
  50. package/dist/types/crypto/index.d.ts +3 -3
  51. package/dist/types/index.d.ts +1 -2
  52. package/package.json +4 -2
  53. package/dist/lib/client/types/identity.js +0 -18
  54. package/dist/types/client/types/identity.d.ts +0 -29
@@ -0,0 +1,84 @@
1
+ import { fileTypeFromBlob } from 'file-type';
2
+ export const downloadFileLite = async (file, name) => {
3
+ const meta = (await fileTypeFromBlob(file instanceof Blob ? file : new Blob([file]))) || { ext: 'dat', mime: 'application/octet-stream' };
4
+ const blob = file instanceof Blob ? file : new Blob([file], { type: meta.mime });
5
+ const link = document.createElement('a');
6
+ link.href = URL.createObjectURL(blob);
7
+ link.download = name; // On ne double pas l’extension ici
8
+ document.body.appendChild(link);
9
+ link.click();
10
+ document.body.removeChild(link);
11
+ URL.revokeObjectURL(link.href); // nettoyage
12
+ };
13
+ export const downloadFileSmart = async (file, name) => {
14
+ if (typeof window === 'undefined') {
15
+ return false;
16
+ }
17
+ const meta = (await fileTypeFromBlob(file instanceof Blob ? file : new Blob([file]))) || { ext: 'dat', mime: 'application/octet-stream' };
18
+ const blob = file instanceof Blob ? file : new Blob([file], { type: meta.mime });
19
+ // 1. Try File System Access API
20
+ if ('showSaveFilePicker' in window) {
21
+ try {
22
+ const handle = await window.showSaveFilePicker({
23
+ suggestedName: name,
24
+ types: [
25
+ {
26
+ description: 'Fichier',
27
+ accept: {
28
+ [blob.type]: [`.${meta.ext}`],
29
+ },
30
+ },
31
+ ],
32
+ });
33
+ const writable = await handle.createWritable();
34
+ await writable.write(blob);
35
+ await writable.close();
36
+ return true;
37
+ }
38
+ catch {
39
+ // Silent failover to fallback
40
+ }
41
+ }
42
+ // 2. Try StreamSaver.js
43
+ try {
44
+ const streamSaver = await import('streamsaver').then((m) => m.default);
45
+ const fileStream = streamSaver.createWriteStream(name, { size: blob.size });
46
+ const readable = blob.stream();
47
+ if (window.WritableStream && readable.pipeTo) {
48
+ await readable.pipeTo(fileStream);
49
+ }
50
+ else {
51
+ const writer = fileStream.getWriter();
52
+ const reader = readable.getReader();
53
+ const pump = async () => {
54
+ const { done, value } = await reader.read();
55
+ if (done) {
56
+ return writer.close();
57
+ }
58
+ await writer.write(value);
59
+ return pump();
60
+ };
61
+ await pump();
62
+ }
63
+ return true;
64
+ }
65
+ catch {
66
+ // Silent failover to anchor
67
+ }
68
+ // 3. Fallback: anchor download
69
+ try {
70
+ const url = URL.createObjectURL(blob);
71
+ const a = document.createElement('a');
72
+ a.href = url;
73
+ a.download = name;
74
+ a.style.display = 'none';
75
+ document.body.appendChild(a);
76
+ a.click();
77
+ document.body.removeChild(a);
78
+ URL.revokeObjectURL(url);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ };
@@ -21,27 +21,26 @@ export function getSecrecyClient(opts = {}) {
21
21
  const storage = getStorage(opts.session);
22
22
  const infos = parseInfos();
23
23
  if (infos !== null) {
24
- storage.identities.save(infos.identities);
25
- storage.keyPairs.save(infos.keyPairs);
24
+ storage.userAppKeys.save(infos.keys);
26
25
  storage.userAppSession.save(infos.uaSession);
27
26
  storage.jwt.save(infos.jwt);
28
27
  return new SecrecyClient({
29
28
  uaSession: infos.uaSession,
30
- identities: infos.identities,
31
- keyPairs: infos.keyPairs,
29
+ uaKeys: infos.keys,
32
30
  uaJwt: infos.jwt,
33
31
  secrecyUrls: opts.secrecyUrls,
34
32
  });
35
33
  }
36
34
  const uaSession = storage.userAppSession.load();
37
- const identities = storage.identities.load();
38
- const keyPairs = storage.keyPairs.load();
35
+ const uaKeys = storage.userAppKeys.load();
39
36
  const uaJwt = storage.jwt.load();
40
- if (uaSession && identities && keyPairs && uaJwt) {
37
+ if (uaSession && uaKeys && uaJwt) {
38
+ storage.userAppKeys.save(uaKeys);
39
+ storage.userAppSession.save(uaSession);
40
+ storage.jwt.save(uaJwt);
41
41
  return new SecrecyClient({
42
42
  uaSession,
43
- identities,
44
- keyPairs,
43
+ uaKeys,
45
44
  uaJwt,
46
45
  secrecyUrls: opts.secrecyUrls,
47
46
  });
@@ -51,7 +50,7 @@ export function getSecrecyClient(opts = {}) {
51
50
  export async function login({ appId, context, path, redirect, scopes, backPath, session, secrecyUrls, }) {
52
51
  return await new Promise(async (resolve, reject) => {
53
52
  const appUrl = window.location.origin;
54
- const client = getSecrecyClient({ session, secrecyUrls });
53
+ const client = getSecrecyClient({ secrecyUrls });
55
54
  const innerLogin = () => {
56
55
  const infos = {
57
56
  appUrl,
@@ -68,13 +67,11 @@ export async function login({ appId, context, path, redirect, scopes, backPath,
68
67
  const validate = (infos) => {
69
68
  const storage = getStorage(session);
70
69
  storage.userAppSession.save(infos.uaSession);
71
- storage.identities.save(infos.identities);
72
- storage.keyPairs.save(infos.keyPairs);
70
+ storage.userAppKeys.save(infos.keys);
73
71
  storage.jwt.save(infos.jwt);
74
72
  resolve(new SecrecyClient({
75
73
  uaSession: infos.uaSession,
76
- identities: infos.identities,
77
- keyPairs: infos.keyPairs,
74
+ uaKeys: infos.keys,
78
75
  uaJwt: infos.jwt,
79
76
  secrecyUrls,
80
77
  }));
@@ -114,10 +111,6 @@ export async function login({ appId, context, path, redirect, scopes, backPath,
114
111
  }
115
112
  else {
116
113
  if (context) {
117
- if (context.userId && client.uaIdentity.userId !== context.userId) {
118
- innerLogin();
119
- return;
120
- }
121
114
  const me = await client.me();
122
115
  if ((context.userId && context.userId !== me.id) ||
123
116
  (context.orgId && context.orgId !== me.organization.id)) {
@@ -11,9 +11,7 @@ import { SecrecyPseudonymClient } from './SecrecyPseudonymClient.js';
11
11
  import { decryptAnonymous } from '../crypto/index.js';
12
12
  import { SecrecyOrganizationClient } from './SecrecyOrganizationClient.js';
13
13
  export class SecrecyClient extends BaseClient {
14
- #groupIdentities;
15
- #uaIdentity;
16
- #keyPairs;
14
+ #keys;
17
15
  cloud;
18
16
  mail;
19
17
  app;
@@ -38,53 +36,22 @@ export class SecrecyClient extends BaseClient {
38
36
  }
39
37
  },
40
38
  });
41
- this.#keyPairs = opts.keyPairs;
42
- this.#groupIdentities = opts.identities.filter((i) => i.kind === 'GROUP');
43
- const uaIdentities = opts.identities.filter((i) => i.kind === 'USER_APP');
44
- if (uaIdentities.length !== 1) {
45
- throw new Error('One USER_APP identity is required');
46
- }
47
- this.#uaIdentity = uaIdentities[0];
48
- this.cloud = new SecrecyCloudClient(this);
49
- this.mail = new SecrecyMailClient(this);
50
- this.app = new SecrecyAppClient(opts.uaJwt, this);
51
- this.db = new SecrecyDbClient(this);
52
- this.organization = new SecrecyOrganizationClient(this);
39
+ this.#keys = opts.uaKeys;
40
+ this.cloud = new SecrecyCloudClient(this, this.#keys, this.client);
41
+ this.mail = new SecrecyMailClient(this, this.#keys, this.client);
42
+ this.app = new SecrecyAppClient(opts.uaJwt, this, this.#keys, this.client);
43
+ this.db = new SecrecyDbClient(this, this.#keys, this.client);
44
+ this.organization = new SecrecyOrganizationClient(this, this.#keys, this.client);
53
45
  this.wallet = new SecrecyWalletClient(this);
54
- this.pay = new SecrecyPayClient(this);
55
- this.user = new SecrecyUserClient(this);
56
- this.pseudonym = new SecrecyPseudonymClient(this);
46
+ this.pay = new SecrecyPayClient(this, this.#keys, this.client);
47
+ this.user = new SecrecyUserClient(this, this.#keys, this.client);
48
+ this.pseudonym = new SecrecyPseudonymClient(this, this.#keys, this.client);
57
49
  }
58
50
  get publicKey() {
59
- return this.#uaIdentity.identityPubKey;
60
- }
61
- get apiClient() {
62
- return this.client;
63
- }
64
- get keyPairs() {
65
- return this.#keyPairs;
66
- }
67
- getPrivateKey(pubKey) {
68
- const privateKey = this.#keyPairs[pubKey];
69
- if (privateKey === undefined) {
70
- throw new Error(`Missing private key for public key ${pubKey}`);
71
- }
72
- return privateKey;
73
- }
74
- get uaPrivateKey() {
75
- return this.getPrivateKey(this.#uaIdentity.identityPubKey);
76
- }
77
- get groupIdentities() {
78
- return this.#groupIdentities;
79
- }
80
- get uaIdentity() {
81
- return this.#uaIdentity;
51
+ return this.#keys.publicKey;
82
52
  }
83
53
  decryptAnonymous(data) {
84
- return decryptAnonymous(data, {
85
- publicKey: this.#uaIdentity.identityPubKey,
86
- privateKey: this.uaPrivateKey,
87
- });
54
+ return decryptAnonymous(data, this.#keys);
88
55
  }
89
56
  async logout(sessionId) {
90
57
  nodesCache.clear();
@@ -92,7 +59,4 @@ export class SecrecyClient extends BaseClient {
92
59
  publicKeysCache.clear();
93
60
  await super.logout(sessionId);
94
61
  }
95
- async getIdentities(input) {
96
- return await this.client.identity.getMany.query(input);
97
- }
98
62
  }
@@ -1,8 +1,7 @@
1
1
  import { storeBuddy } from '../utils/store-buddy.js';
2
2
  export function getStorage(session) {
3
3
  const userAppSession = storeBuddy(`secrecy.user_app_session`, session).init(null);
4
- const identities = storeBuddy(`secrecy.identities`, session).init(null);
5
- const keyPairs = storeBuddy(`secrecy.key_pairs`, session).init(null);
4
+ const userAppKeys = storeBuddy(`secrecy.user_app_keys`, session).init(null);
6
5
  const jwt = storeBuddy(`secrecy.jwt`, session).init(null);
7
- return { identities, keyPairs, userAppSession, jwt };
6
+ return { userAppKeys, userAppSession, jwt };
8
7
  }
@@ -1,9 +1,13 @@
1
1
  import { z } from 'zod';
2
- import { accessIdentitySchema } from './identity.js';
2
+ const keyPair = z
3
+ .object({
4
+ publicKey: z.string(),
5
+ privateKey: z.string(),
6
+ })
7
+ .strict();
3
8
  export const secrecyUserApp = z
4
9
  .object({
5
- identities: accessIdentitySchema.array(),
6
- keyPairs: z.record(z.string(), z.string()),
10
+ keys: keyPair,
7
11
  jwt: z.string(),
8
12
  uaSession: z.string(),
9
13
  })
@@ -0,0 +1,252 @@
1
+ import { kiloToBytes } from '../utils.js';
2
+ import { compress } from '../minify/index.js';
3
+ import { md5 } from '../worker/md5.js';
4
+ import { sodium } from '../sodium.js';
5
+ import { fileTypeFromBuffer } from 'file-type';
6
+ import { dataContentCache } from '../cache.js';
7
+ import { chunks, enumerate } from '../utils/array.js';
8
+ import { getTrpcGuestClient, } from '../client.js';
9
+ import axios from 'axios';
10
+ import { promiseAllLimit } from '../utils/promise.js';
11
+ import { encryptDataAndKey } from '../crypto/domain.js';
12
+ import { derivePassword, generatePassword } from '../crypto/helpers.js';
13
+ import { decryptCryptoBox, encryptSecretBox } from '../crypto/index.js';
14
+ export async function uploadData({ storageType, data, password, forcePassword = false, encrypted = true, encryptProgress, uploadProgress, signal, meta, keyPair, apiClient, }) {
15
+ if (!encrypted && (password || forcePassword)) {
16
+ throw new Error('Cannot share unencrypted data with a password!');
17
+ }
18
+ if (encrypted && !password && !forcePassword && !keyPair) {
19
+ throw new Error('Cannot share encrypted data without a password!');
20
+ }
21
+ apiClient ??= getTrpcGuestClient();
22
+ const dataBuffer = data instanceof File
23
+ ? new Uint8Array(await data.arrayBuffer())
24
+ : typeof window === 'undefined' && data instanceof Buffer
25
+ ? new Uint8Array(data)
26
+ : data;
27
+ let filetype;
28
+ if (meta === true) {
29
+ filetype = await fileTypeFromBuffer(dataBuffer);
30
+ }
31
+ else if (typeof meta !== 'undefined') {
32
+ filetype = meta;
33
+ }
34
+ else if (!encrypted) {
35
+ filetype = await fileTypeFromBuffer(dataBuffer);
36
+ }
37
+ if (storageType === 'lite' && dataBuffer.byteLength > kiloToBytes(1024)) {
38
+ throw new Error('The data is too big for lite upload!');
39
+ }
40
+ if (!keyPair && storageType === 'cold') {
41
+ throw new Error('Cold storage is only for logged users!');
42
+ }
43
+ const compressed = encrypted ? compress(dataBuffer) : dataBuffer;
44
+ let { encryptedData, encryptedDataKey, dataKey, md5Data, md5Encrypted } = encrypted
45
+ ? await encryptDataAndKey({
46
+ data: compressed,
47
+ progress: encryptProgress,
48
+ keyPair,
49
+ signal,
50
+ })
51
+ : {
52
+ encryptedData: compressed,
53
+ encryptedDataKey: null,
54
+ dataKey: null,
55
+ md5Data: await md5(compressed),
56
+ md5Encrypted: null,
57
+ };
58
+ await uploadProgress?.({
59
+ total: encryptedData.byteLength,
60
+ current: 0,
61
+ percent: 0,
62
+ });
63
+ const createSharing = ({ data, }) => {
64
+ if (dataKey && (password || forcePassword)) {
65
+ password ??= generatePassword();
66
+ const salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, password + md5Data);
67
+ const derivedPassword = derivePassword(password, salt);
68
+ let key;
69
+ if (data.type === 'guest' ||
70
+ (data.type === 'authed' &&
71
+ encryptedDataKey &&
72
+ data.key === sodium.to_hex(encryptedDataKey))) {
73
+ key = dataKey;
74
+ }
75
+ else {
76
+ if (!keyPair) {
77
+ throw new Error('Unable to encrypt data without keyPair!');
78
+ }
79
+ if (!data.key) {
80
+ throw new Error('Unable to encrypt data without key!');
81
+ }
82
+ key = decryptCryptoBox(sodium.from_hex(data.key), data.keyPair.pub, keyPair.privateKey);
83
+ }
84
+ // NOTE: Process to create a sharing for a auth client (todo: endpoint)
85
+ return {
86
+ password,
87
+ encryptedDataKey: sodium.to_hex(encryptSecretBox(key, derivedPassword)),
88
+ };
89
+ }
90
+ return null;
91
+ };
92
+ if (storageType === 'lite') {
93
+ const uploadDataArgs = md5Encrypted
94
+ ? {
95
+ type: 'encrypted',
96
+ content: Buffer.from(encryptedData),
97
+ sizeEncrypted: BigInt(encryptedData.byteLength),
98
+ size: BigInt(dataBuffer.byteLength),
99
+ key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
100
+ md5Encrypted,
101
+ md5: md5Data,
102
+ ...filetype,
103
+ }
104
+ : {
105
+ type: 'unencrypted',
106
+ content: Buffer.from(encryptedData),
107
+ md5: md5Data,
108
+ sizeEncrypted: undefined,
109
+ size: BigInt(dataBuffer.byteLength),
110
+ ...filetype,
111
+ };
112
+ const uploadData = await apiClient.cloud.uploadLiteData.mutate(uploadDataArgs, { signal });
113
+ await uploadProgress?.({
114
+ total: encryptedData.byteLength,
115
+ current: encryptedData.byteLength,
116
+ percent: 1,
117
+ });
118
+ const sharing = createSharing({ data: uploadData });
119
+ const localData = {
120
+ id: uploadData.id,
121
+ storageType: 'lite',
122
+ size: uploadDataArgs.size,
123
+ sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
124
+ data: dataBuffer,
125
+ ...filetype,
126
+ };
127
+ dataContentCache.set(uploadData.id, localData);
128
+ return {
129
+ ...localData,
130
+ sharing,
131
+ };
132
+ }
133
+ if (storageType === 's3' || storageType === 'cold') {
134
+ const uploadDataArgs = md5Encrypted
135
+ ? {
136
+ type: 'encrypted',
137
+ sizeEncrypted: BigInt(encryptedData.byteLength),
138
+ size: BigInt(dataBuffer.byteLength),
139
+ key: encryptedDataKey ? sodium.to_hex(encryptedDataKey) : undefined,
140
+ md5Encrypted,
141
+ md5: md5Data,
142
+ ...filetype,
143
+ }
144
+ : {
145
+ type: 'unencrypted',
146
+ md5: md5Data,
147
+ size: BigInt(dataBuffer.byteLength),
148
+ sizeEncrypted: undefined,
149
+ ...filetype,
150
+ };
151
+ const uploadDataCaller = storageType === 's3'
152
+ ? apiClient.cloud.uploadData
153
+ : apiClient.cloud.uploadColdData;
154
+ const uploadData = await uploadDataCaller.mutate(uploadDataArgs, {
155
+ signal,
156
+ });
157
+ if (uploadData.parts.length === 0) {
158
+ if (uploadData.type === 'authed' &&
159
+ (typeof keyPair === 'undefined' ||
160
+ typeof keyPair === 'string' ||
161
+ uploadData.keyPair.pub !== keyPair.publicKey)) {
162
+ throw new Error('The public key does not match with cached key!');
163
+ }
164
+ await uploadProgress?.({
165
+ total: encryptedData.byteLength,
166
+ current: encryptedData.byteLength,
167
+ percent: 1,
168
+ });
169
+ const sharing = createSharing({ data: uploadData });
170
+ const localData = {
171
+ id: uploadData.id,
172
+ storageType: storageType,
173
+ size: uploadDataArgs.size,
174
+ sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
175
+ data: dataBuffer,
176
+ ...filetype,
177
+ };
178
+ dataContentCache.set(uploadData.id, localData);
179
+ return {
180
+ ...localData,
181
+ sharing,
182
+ };
183
+ }
184
+ const uploadDataPartEnd = async (md5, order) => {
185
+ return apiClient.cloud.uploadDataPartEnd.mutate({ dataId: uploadData.id, md5, order }, { signal });
186
+ };
187
+ const chunkParts = new Array();
188
+ for (const [index, chunk] of enumerate(chunks(encryptedData, Number(uploadData.partSize)))) {
189
+ chunkParts.push({
190
+ order: index + 1,
191
+ data: chunk,
192
+ md5: await md5(chunk),
193
+ });
194
+ }
195
+ const progressParts = {};
196
+ const onProgress = (part, progressEvent) => {
197
+ progressParts[part] = progressEvent;
198
+ const current = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
199
+ void uploadProgress?.({
200
+ percent: current / encryptedData.byteLength,
201
+ total: encryptedData.byteLength,
202
+ current,
203
+ });
204
+ };
205
+ const byPart = async (part) => {
206
+ const formData = new FormData();
207
+ const chunk = chunkParts.find((p) => p.order === part.order);
208
+ if (chunk === undefined) {
209
+ return;
210
+ }
211
+ for (const [key, value] of Object.entries(part.fields)) {
212
+ formData.append(key, value);
213
+ }
214
+ formData.append('file', new Blob([chunk.data], { type: filetype?.mime }), `${uploadData.id}-${chunk.order}`);
215
+ await axios.post(part.url, formData, {
216
+ onUploadProgress: (progressEvent) => {
217
+ onProgress(part.order, {
218
+ percent: progressEvent.progress ?? 0,
219
+ totalBytes: progressEvent.total ?? 0,
220
+ transferredBytes: progressEvent.loaded,
221
+ });
222
+ },
223
+ });
224
+ return uploadDataPartEnd(chunk.md5, chunk.order);
225
+ };
226
+ await promiseAllLimit(3, uploadData.parts.map((p) => async () => {
227
+ await byPart(p);
228
+ }));
229
+ const sharing = createSharing({ data: uploadData });
230
+ const localData = {
231
+ id: uploadData.id,
232
+ storageType: storageType,
233
+ size: uploadDataArgs.size,
234
+ sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
235
+ data: dataBuffer,
236
+ ...filetype,
237
+ };
238
+ dataContentCache.set(uploadData.id, localData);
239
+ return {
240
+ ...localData,
241
+ sharing,
242
+ };
243
+ }
244
+ throw new Error(`The "${storageType}" is not implemented yet!`);
245
+ }
246
+ export function createPublicDataLink({ apiClient, ...opts }) {
247
+ if (opts.expireAt && opts.expireAt <= new Date()) {
248
+ throw new Error('Unable to create public link using a past expireAt date!');
249
+ }
250
+ apiClient ??= getTrpcGuestClient();
251
+ return apiClient.cloud.createDataLink.mutate(opts);
252
+ }
@@ -48,3 +48,10 @@ export const createTRPCClient = (opts) => innerCreateTRPCClient({
48
48
  }),
49
49
  ],
50
50
  });
51
+ export const getTrpcGuestClient = (() => {
52
+ let client;
53
+ return function getApiGuestClient({ url } = {}) {
54
+ client ??= createTRPCClient({ apiUrl: url });
55
+ return client;
56
+ };
57
+ })();
@@ -122,6 +122,9 @@ export async function decryptSecretStream(key, data, progress, abort) {
122
122
  throw new Error(`Decrypt aborted`);
123
123
  }
124
124
  const messageTag = decryptFn(chunk);
125
+ if (typeof messageTag === 'boolean') {
126
+ throw new Error('Unable to decrypt the data!');
127
+ }
125
128
  final.set(messageTag.message, total);
126
129
  total += messageTag.message.byteLength;
127
130
  const percent = total / max;
@@ -1,17 +1,128 @@
1
1
  import { encryptCryptoBox } from '.';
2
- import { sodium } from '../sodium';
3
- import { encryptSecretStream, secretStreamKeygen } from './data';
4
- export async function encryptName(name, nameKey) {
5
- const { data } = await encryptSecretStream(sodium.from_hex(nameKey), sodium.from_string(name));
6
- return sodium.to_hex(data);
2
+ import { secretStreamKeygen } from './data';
3
+ import { encrypt } from '../worker/sodium';
4
+ import ky from 'ky';
5
+ import { md5 } from '../worker/md5.js';
6
+ import { promiseAllLimit } from '../utils/promise.js';
7
+ import { concatenate } from '../utils/array.js';
8
+ /**
9
+ * Encrypt the dataKey and the data as logged or guest user.
10
+ * If a string is provided as keypair, it should be considered as guest with password case.
11
+ * If keypair is not provided, then we generate a key to be used as password for guest too.
12
+ */
13
+ export async function encryptDataAndKey({ data, keyPair, progress, signal, }) {
14
+ const dataKey = secretStreamKeygen();
15
+ const { data: encryptedData, md5: md5Data, md5Encrypted, } = await encrypt(dataKey, data, progress, signal);
16
+ if (!keyPair) {
17
+ return {
18
+ encryptedData,
19
+ dataKey,
20
+ md5Data,
21
+ md5Encrypted,
22
+ };
23
+ }
24
+ const encDataKey = encryptCryptoBox(dataKey, keyPair.publicKey, keyPair.privateKey);
25
+ return {
26
+ encryptedDataKey: encDataKey,
27
+ encryptedData,
28
+ dataKey,
29
+ md5Data,
30
+ md5Encrypted,
31
+ };
7
32
  }
8
- export async function generateAndEncryptNameAndKey(args) {
9
- const nameKey = secretStreamKeygen();
10
- const encryptedName = await encryptName(args.name, sodium.to_hex(nameKey));
11
- const encryptedNameKey = sodium.to_hex(encryptCryptoBox(nameKey, args.publicKey, args.privateKey));
33
+ const encryptedContentFromParts = async (arg) => {
34
+ const parts = {};
35
+ const byPart = async (part) => {
36
+ const buf = new Uint8Array(await ky
37
+ .get(part.contentUrl, {
38
+ timeout: false,
39
+ onDownloadProgress: (pr) => {
40
+ arg.onProgress(`${arg.dataId}-${part.order}`, pr);
41
+ },
42
+ signal: arg.signal,
43
+ })
44
+ .arrayBuffer());
45
+ const md5Part = await md5(buf);
46
+ if (md5Part !== part.md5) {
47
+ throw new Error(`Invalid md5 for part ${part.order} of data ${arg.dataId}`);
48
+ }
49
+ if (typeof parts[arg.dataId] === 'undefined') {
50
+ parts[arg.dataId] = [{ data: buf, order: part.order }];
51
+ }
52
+ else {
53
+ parts[arg.dataId].push({ data: buf, order: part.order });
54
+ }
55
+ };
56
+ await promiseAllLimit(3, arg.dataParts.map((p) => async () => byPart(p)));
57
+ return concatenate(...parts[arg.dataId].sort((a, b) => a.order - b.order).map((p) => p.data));
58
+ };
59
+ export async function buildBytesFromApiData({ dataContent, totalBytes, progressParts, onDownloadProgress, signal, }) {
60
+ const onProgress = (key, progressEvent) => {
61
+ progressParts[key] = progressEvent;
62
+ const transferredBytes = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
63
+ onDownloadProgress?.({
64
+ percent: transferredBytes / totalBytes,
65
+ totalBytes,
66
+ transferredBytes,
67
+ });
68
+ };
69
+ const encryptedContent = dataContent.type === 'lite'
70
+ ? new Uint8Array(dataContent.content)
71
+ : dataContent.type === 'cloud'
72
+ ? await encryptedContentFromParts({
73
+ dataId: dataContent.id,
74
+ dataParts: dataContent.parts,
75
+ onProgress,
76
+ signal,
77
+ })
78
+ : dataContent.maybeContent !== null
79
+ ? new Uint8Array(dataContent.maybeContent)
80
+ : dataContent.maybeParts !== null
81
+ ? await encryptedContentFromParts({
82
+ dataId: dataContent.id,
83
+ dataParts: dataContent.maybeParts,
84
+ onProgress,
85
+ signal,
86
+ })
87
+ : null;
88
+ if (encryptedContent === null) {
89
+ throw `Can't find content for data ${dataContent.id}`;
90
+ }
91
+ const md5Encrypted = await md5(encryptedContent);
92
+ if (md5Encrypted !== dataContent.md5Encrypted) {
93
+ throw new Error(`Encrypted content does not match`);
94
+ }
12
95
  return {
13
- nameKey,
14
- encryptedName,
15
- encryptedNameKey,
96
+ encryptedContent,
97
+ };
98
+ }
99
+ export async function buildBytesFromDataLink({ dataLink, totalBytes, onDownloadProgress, signal, }) {
100
+ const progressParts = {};
101
+ const onProgress = (key, progressEvent) => {
102
+ progressParts[key] = progressEvent;
103
+ const transferredBytes = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
104
+ onDownloadProgress?.({
105
+ percent: transferredBytes / totalBytes,
106
+ totalBytes,
107
+ transferredBytes,
108
+ });
16
109
  };
110
+ const bytes = 'bytes' in dataLink
111
+ ? Uint8Array.from(Buffer.from(dataLink.bytes, 'base64'))
112
+ : await encryptedContentFromParts({
113
+ dataId: Math.random().toString().slice(2, 10),
114
+ dataParts: dataLink.parts,
115
+ onProgress,
116
+ signal,
117
+ });
118
+ if (bytes === null) {
119
+ throw `Can't find content for data ${dataLink.name}`;
120
+ }
121
+ if (dataLink.md5Encrypted) {
122
+ const md5Encrypted = await md5(bytes);
123
+ if (md5Encrypted !== dataLink.md5Encrypted) {
124
+ throw new Error(`Encrypted content does not match`);
125
+ }
126
+ }
127
+ return bytes;
17
128
  }