@secrecy/lib 1.62.0-feat-node-sharing.3 → 1.62.0-feat-node-sharing.5

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.
@@ -76,12 +76,12 @@ export class SecrecyAppClient {
76
76
  ...new Set(userIds.filter((userId) => publicKeys[userId] === undefined)),
77
77
  ];
78
78
  if (missingKeys.length > 0) {
79
- const userKeysMap = await this.#apiClient.application.userPublicKey.query(userIds);
79
+ const userKeysMap = await this.#apiClient.application.userPublicKey.query(missingKeys);
80
80
  if ('publicKey' in userKeysMap) {
81
81
  throw Error('Should not happen!');
82
82
  }
83
83
  if (Object.keys(userKeysMap).length !== missingKeys.length) {
84
- throw new Error("Unable to load some user's public keys!");
84
+ throw new Error("Unable to load some user's public keys!");
85
85
  }
86
86
  for (const userId in userKeysMap) {
87
87
  publicKeys[userId] = userKeysMap[userId];
@@ -14,6 +14,7 @@ import { promiseAllLimit } from '../utils/promise.js';
14
14
  import { kiloToBytes } from '../utils.js';
15
15
  import { fileTypeFromBuffer } from 'file-type';
16
16
  import { encryptName, generateAndEncryptNameAndKey } from '../crypto/domain.js';
17
+ import { chunkByTotalItems } from '../utils/object.js';
17
18
  export class SecrecyCloudClient {
18
19
  #client;
19
20
  #keys;
@@ -32,18 +33,24 @@ export class SecrecyCloudClient {
32
33
  const data = node.history.find((d) => d.id === dataId);
33
34
  if (data !== undefined) {
34
35
  const users = node.users.filter(([u]) => u.id !== this.#client.app.userId);
36
+ const userIds = users.map(([user]) => user.id);
37
+ const userKeys = await this.#client.app.userPublicKey(userIds);
38
+ const shares = users.map(([user]) => {
39
+ const publicKey = userKeys[user.id];
40
+ if (!publicKey) {
41
+ throw new Error('Unable to retreive share by public key!');
42
+ }
43
+ return {
44
+ id: user.id,
45
+ key: data.key
46
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), publicKey, this.#keys.privateKey))
47
+ : null,
48
+ };
49
+ });
35
50
  await this.#apiClient.cloud.shareDataInHistory.mutate({
36
51
  dataId: data.id,
37
52
  nodeId,
38
- users: await Promise.all(users.map(async ([u]) => {
39
- const userPubKey = await this.#client.app.userPublicKey(u.id);
40
- return {
41
- id: u.id,
42
- key: data.key
43
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), userPubKey, this.#keys.privateKey))
44
- : null,
45
- };
46
- })),
53
+ users: shares,
47
54
  });
48
55
  }
49
56
  return internalNodeFullToNodeFull(node);
@@ -318,7 +325,7 @@ export class SecrecyCloudClient {
318
325
  });
319
326
  return apiDataToExternal(data, this.#keys);
320
327
  }
321
- async shareNode(input) {
328
+ async shareNode(input, progress) {
322
329
  const userIds = 'rights' in input
323
330
  ? Array.isArray(input.nodes)
324
331
  ? input.nodes.map(({ userId }) => userId)
@@ -326,27 +333,70 @@ export class SecrecyCloudClient {
326
333
  ? input.nodes.userIds
327
334
  : [input.nodes.userId]
328
335
  : input.users.map(({ id }) => id);
329
- const nodesToShare = 'nodes' in input
330
- ? input.nodes
331
- : {
332
- nodeId: input.nodeId,
333
- userIds: input.users.map(({ id }) => id),
334
- };
335
336
  const [publicKeysMap, nodesIdsMap] = await Promise.all([
336
337
  this.#client.app.userPublicKey(userIds),
337
- this.#apiClient.cloud.shareNode.mutate(nodesToShare),
338
+ this.#apiClient.cloud.shareNode.mutate('nodes' in input
339
+ ? input.nodes
340
+ : {
341
+ nodeId: input.nodeId,
342
+ userIds: input.users.map(({ id }) => id),
343
+ }),
338
344
  ]);
339
- const nodesInfos = await this.encryptNodesForUsers(nodesIdsMap, publicKeysMap);
340
- const sharingState = await this.#apiClient.cloud.shareNodeFinish.mutate(Object.entries(nodesInfos).map('rights' in input
341
- ? ([userId, nodes]) => ({ userId, nodes, rights: input.rights })
342
- : ([userId, nodes]) => {
343
- const user = input.users.find((user) => user.id === userId);
344
- if (!user) {
345
- throw new Error(`Unable to find rights for user: ${userId}`);
346
- }
347
- return { userId, nodes, rights: user.rights };
348
- }));
349
- return sharingState;
345
+ const maxNodesBatchSize = 1000;
346
+ const totalNodesToShare = Object.values(nodesIdsMap).reduce((size, ids) => size + ids.length, 0);
347
+ const chunks = totalNodesToShare > maxNodesBatchSize
348
+ ? chunkByTotalItems(nodesIdsMap, maxNodesBatchSize)
349
+ : [nodesIdsMap];
350
+ const details = await chunks.reduce(async (pendingState, nodesMap, index) => {
351
+ const state = await pendingState;
352
+ const infos = await this.encryptNodesForUsers(nodesMap, publicKeysMap);
353
+ const subState = await this.#apiClient.cloud.shareNodeFinish.mutate(Object.entries(infos).map('rights' in input
354
+ ? ([userId, nodes]) => ({ userId, nodes, rights: input.rights })
355
+ : ([userId, nodes]) => {
356
+ const user = input.users.find((user) => user.id === userId);
357
+ if (!user) {
358
+ throw new Error(`Unable to find rights for user: ${userId}`);
359
+ }
360
+ return { userId, nodes, rights: user.rights };
361
+ }));
362
+ const currentProgress = Math.min((index + 1) * maxNodesBatchSize, totalNodesToShare);
363
+ progress?.({
364
+ total: totalNodesToShare,
365
+ current: currentProgress,
366
+ percent: Math.round((currentProgress / totalNodesToShare) * 100),
367
+ });
368
+ return {
369
+ missingNodeAccesses: [
370
+ ...state.missingNodeAccesses,
371
+ ...(subState.isFinished
372
+ ? []
373
+ : subState.details.missingNodeAccesses),
374
+ ],
375
+ missingDataAccesses: [
376
+ ...state.missingDataAccesses,
377
+ ...(subState.isFinished
378
+ ? []
379
+ : subState.details.missingDataAccesses),
380
+ ],
381
+ invalidRightsAccesses: [
382
+ ...state.invalidRightsAccesses,
383
+ ...(subState.isFinished
384
+ ? []
385
+ : subState.details.invalidRightsAccesses),
386
+ ],
387
+ };
388
+ }, Promise.resolve({
389
+ missingNodeAccesses: [],
390
+ missingDataAccesses: [],
391
+ invalidRightsAccesses: [],
392
+ }));
393
+ const errorDetailsLength = details.invalidRightsAccesses.length +
394
+ details.missingDataAccesses.length +
395
+ details.missingNodeAccesses.length;
396
+ return {
397
+ isFinished: errorDetailsLength === 0,
398
+ details: details,
399
+ };
350
400
  }
351
401
  async updateNode({ nodeId, name, isFavorite, deletedAt, }) {
352
402
  let node = nodesCache.get(nodeId);
@@ -0,0 +1,29 @@
1
+ export function chunkByTotalItems(input, maxItemsPerChunk = 1000) {
2
+ const entries = Object.entries(input);
3
+ const chunks = [];
4
+ let currentChunk = {};
5
+ let currentCount = 0;
6
+ for (const [key, values] of entries) {
7
+ let i = 0;
8
+ while (i < values.length) {
9
+ const remaining = maxItemsPerChunk - currentCount;
10
+ const take = Math.min(remaining, values.length - i);
11
+ if (!currentChunk[key]) {
12
+ currentChunk[key] = [];
13
+ }
14
+ currentChunk[key].push(...values.slice(i, i + take));
15
+ currentCount += take;
16
+ i += take;
17
+ if (currentCount === maxItemsPerChunk) {
18
+ chunks.push(currentChunk);
19
+ currentChunk = {};
20
+ currentCount = 0;
21
+ }
22
+ }
23
+ }
24
+ // Last chunk
25
+ if (currentCount > 0) {
26
+ chunks.push(currentChunk);
27
+ }
28
+ return chunks;
29
+ }
@@ -75,7 +75,7 @@ export declare class SecrecyCloudClient {
75
75
  id: string;
76
76
  rights: Rights;
77
77
  }[];
78
- }): Promise<RouterOutputs['cloud']['shareNodeFinish']>;
78
+ }, progress?: ProgressCallback): Promise<RouterOutputs['cloud']['shareNodeFinish']>;
79
79
  updateNode({ nodeId, name, isFavorite, deletedAt, }: {
80
80
  nodeId: string;
81
81
  name?: string | null | undefined;
@@ -70,4 +70,21 @@ export type EncryptedNodeInfos = {
70
70
  key: string | null;
71
71
  }[];
72
72
  };
73
+ export type ShareNodeDetails = {
74
+ missingNodeAccesses: {
75
+ userId: string;
76
+ nodeId: string;
77
+ }[];
78
+ missingDataAccesses: {
79
+ userId: string;
80
+ dataId: string;
81
+ nodeId: string;
82
+ }[];
83
+ invalidRightsAccesses: {
84
+ userId: string;
85
+ current: Rights;
86
+ nodeId: string;
87
+ expect: Rights;
88
+ }[];
89
+ };
73
90
  export {};
@@ -0,0 +1 @@
1
+ export declare function chunkByTotalItems<T>(input: Record<string, T[]>, maxItemsPerChunk?: number): Record<string, T[]>[];
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@secrecy/lib",
3
3
  "author": "Anonymize <anonymize@gmail.com>",
4
4
  "description": "Anonymize Secrecy Library",
5
- "version": "1.62.0-feat-node-sharing.3",
5
+ "version": "1.62.0-feat-node-sharing.5",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/anonymize-org/lib.git"
@@ -1,140 +0,0 @@
1
- // @ts-nocheck
2
- function* chunks(arr, n) {
3
- for (let i = 0; i < arr.length; i += n) {
4
- yield arr.slice(i, i + n);
5
- }
6
- }
7
- function assert(c, message) {
8
- if (!c) {
9
- throw new Error(message);
10
- }
11
- }
12
- function encrypt(key) {
13
- let destroyed = false;
14
- const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
15
- const encrypt = (tag, plaintext) => {
16
- assert(destroyed === false, 'state already destroyed');
17
- return sodium.crypto_secretstream_xchacha20poly1305_push(state, plaintext, null, tag);
18
- };
19
- function destroy() {
20
- assert(destroyed === false, 'state already destroyed');
21
- destroyed = true;
22
- }
23
- return {
24
- encrypt,
25
- destroy,
26
- header,
27
- };
28
- }
29
- function decrypt(header, key) {
30
- assert(header.byteLength >=
31
- sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES, 'header must be at least HEADERBYTES (' +
32
- sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES +
33
- ') long');
34
- assert(key.byteLength >= sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES, 'key must be at least KEYBYTES (' +
35
- sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES +
36
- ') long');
37
- let destroyed = false;
38
- const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
39
- const decrypt = (ciphertext) => {
40
- assert(destroyed === false, 'state already destroyed');
41
- return sodium.crypto_secretstream_xchacha20poly1305_pull(state, ciphertext);
42
- };
43
- function destroy() {
44
- assert(destroyed === false, 'state already destroyed');
45
- destroyed = true;
46
- }
47
- return {
48
- decrypt,
49
- destroy,
50
- };
51
- }
52
- const CHUNK_SIZE = 8192;
53
- export async function encryptSecretstream(key, data, progress) {
54
- const { encrypt: crypt, destroy, header } = encrypt(key);
55
- const cryptedChunk = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
56
- const max = Math.ceil(data.byteLength / CHUNK_SIZE) * cryptedChunk + header.byteLength;
57
- progress?.({
58
- percent: 0,
59
- total: max,
60
- current: 0,
61
- });
62
- const final = new Uint8Array(max);
63
- const sparkEncrypted = new SparkMD5.ArrayBuffer();
64
- const spark = new SparkMD5.ArrayBuffer();
65
- final.set(header);
66
- sparkEncrypted.append(header);
67
- let total = header.byteLength;
68
- progress?.({
69
- percent: total / max,
70
- total: max,
71
- current: total,
72
- });
73
- let lastPercent = total / max;
74
- for (const chunk of chunks(data, CHUNK_SIZE)) {
75
- spark.append(chunk);
76
- const tag = chunk.length < CHUNK_SIZE
77
- ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
78
- : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
79
- const crypted = crypt(tag, chunk);
80
- sparkEncrypted.append(crypted);
81
- final.set(crypted, total);
82
- total += crypted.byteLength;
83
- const percent = total / max;
84
- if (percent > lastPercent + 0.01) {
85
- progress?.({
86
- percent,
87
- total: max,
88
- current: total,
89
- });
90
- lastPercent = percent;
91
- }
92
- }
93
- destroy();
94
- 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
- }
105
- export async function decryptSecretstream(key, data, progress) {
106
- const header = data.slice(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
107
- data = data.slice(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
108
- const { decrypt: decryptt, destroy } = decrypt(header, key);
109
- const chunkSize = CHUNK_SIZE + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
110
- const max = Math.ceil(data.byteLength / chunkSize) * CHUNK_SIZE;
111
- progress?.({
112
- percent: 0,
113
- total: max,
114
- current: 0,
115
- });
116
- const final = new Uint8Array(max);
117
- let total = 0;
118
- let lastPercent = total / max;
119
- for (const chunk of chunks(data, chunkSize)) {
120
- const tmp = decryptt(chunk);
121
- final.set(tmp.message, total);
122
- total += tmp.message.byteLength;
123
- const percent = total / max;
124
- if (percent > lastPercent + 0.01) {
125
- progress?.({
126
- percent,
127
- total: max,
128
- current: total,
129
- });
130
- lastPercent = percent;
131
- }
132
- }
133
- destroy();
134
- progress?.({
135
- percent: 1,
136
- total,
137
- current: total,
138
- });
139
- return final.slice(0, total);
140
- }
@@ -1,6 +0,0 @@
1
- export declare function encryptSecretstream(key: any, data: any, progress: any): Promise<{
2
- data: Uint8Array<ArrayBuffer>;
3
- md5Encrypted: any;
4
- md5: any;
5
- }>;
6
- export declare function decryptSecretstream(key: any, data: any, progress: any): Promise<Uint8Array<ArrayBuffer>>;