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

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.
@@ -25,21 +25,23 @@ export class BaseClient {
25
25
  account: opts.secrecyUrls?.account ?? 'https://account.secrecy.tech',
26
26
  api: opts.secrecyUrls?.api ?? 'https://api.secrecy.tech',
27
27
  };
28
- this.client = BaseClient.getBaseClient({
29
- session: opts.session,
30
- apiUrl: this.secrecyUrls.api,
31
- onAccessDenied: async () => {
32
- console.log('[BASE_CLIENT - BEFORE] - Access denied');
33
- await opts.onAccessDenied?.();
34
- console.log('[BASE_CLIENT - AFTER] - Access denied');
35
- try {
36
- await this.logout();
37
- }
38
- finally {
39
- location.reload();
40
- }
41
- },
42
- });
28
+ this.client =
29
+ opts.apiClient ??
30
+ BaseClient.getBaseClient({
31
+ session: opts.session,
32
+ apiUrl: this.secrecyUrls.api,
33
+ onAccessDenied: async () => {
34
+ console.log('[BASE_CLIENT - BEFORE] - Access denied');
35
+ await opts.onAccessDenied?.();
36
+ console.log('[BASE_CLIENT - AFTER] - Access denied');
37
+ try {
38
+ await this.logout();
39
+ }
40
+ finally {
41
+ location.reload();
42
+ }
43
+ },
44
+ });
43
45
  }
44
46
  async logout(sessionId) {
45
47
  if (sessionId === null || sessionId === undefined) {
@@ -72,12 +72,17 @@ export class SecrecyAppClient {
72
72
  publicKeysCache.get(`userPublicKey:${userId}-${appId}`),
73
73
  ])
74
74
  .filter(([_, key]) => !!key));
75
- const missingKeys = userIds.filter((userId) => publicKeys[userId] === undefined);
75
+ const missingKeys = [
76
+ ...new Set(userIds.filter((userId) => publicKeys[userId] === undefined)),
77
+ ];
76
78
  if (missingKeys.length > 0) {
77
79
  const userKeysMap = await this.#apiClient.application.userPublicKey.query(userIds);
78
80
  if ('publicKey' in userKeysMap) {
79
81
  throw Error('Should not happen!');
80
82
  }
83
+ if (Object.keys(userKeysMap).length !== missingKeys.length) {
84
+ throw new Error("Unable to load some user's public keys!");
85
+ }
81
86
  for (const userId in userKeysMap) {
82
87
  publicKeys[userId] = userKeysMap[userId];
83
88
  publicKeysCache.set(`userPublicKey:${userId}-${appId}`, userKeysMap[userId]);
@@ -1,6 +1,5 @@
1
1
  import axios from 'axios';
2
2
  import ky from 'ky';
3
- import { encryptName } from '../index.js';
4
3
  import { nodesCache, dataCache, dataContentCache, nodesEncryptionCache, } from '../cache.js';
5
4
  import { secretStreamKeygen } from '../crypto/data.js';
6
5
  import { decryptCryptoBox, encryptCryptoBox } from '../crypto/index.js';
@@ -10,10 +9,11 @@ import { enumerate, chunks, concatenate } from '../utils/array.js';
10
9
  import { md5 } from '../worker/md5.js';
11
10
  import { decrypt, encrypt } from '../worker/sodium.js';
12
11
  import { apiDataToExternal, apiDataToInternal } from './convert/data.js';
13
- import { apiNodeFullToInternalFull, apiNodeToExternal, apiNodeToExternalNodeFull, internalNodeFullToNodeFull, } from './convert/node.js';
12
+ import { apiNodeForEncryptionToInternal, apiNodeFullToInternalFull, apiNodeToExternal, apiNodeToExternalNodeFull, internalNodeFullToNodeFull, } from './convert/node.js';
14
13
  import { promiseAllLimit } from '../utils/promise.js';
15
14
  import { kiloToBytes } from '../utils.js';
16
15
  import { fileTypeFromBuffer } from 'file-type';
16
+ import { encryptName, generateAndEncryptNameAndKey } from '../crypto/domain.js';
17
17
  export class SecrecyCloudClient {
18
18
  #client;
19
19
  #keys;
@@ -284,27 +284,24 @@ export class SecrecyCloudClient {
284
284
  return isDeleted;
285
285
  }
286
286
  async createFolder({ name, parentFolderId, }) {
287
- const key = secretStreamKeygen();
288
- const encryptedName = await encryptName(name, sodium.to_hex(key));
289
- const encryptedKey = encryptCryptoBox(key, this.#keys.publicKey, this.#keys.privateKey);
290
- const createFolder = await this.#apiClient.cloud.createFolder.mutate({
287
+ const { encryptedNameKey, encryptedName } = await generateAndEncryptNameAndKey({
288
+ name,
289
+ privateKey: this.#keys.privateKey,
290
+ publicKey: this.#keys.publicKey,
291
+ });
292
+ const createdFolder = await this.#apiClient.cloud.createFolder.mutate({
291
293
  name: encryptedName,
292
294
  parentId: parentFolderId ?? null,
293
- nameKey: sodium.to_hex(encryptedKey),
295
+ nameKey: sodium.to_hex(encryptedNameKey),
294
296
  });
295
- const folder = await apiNodeToExternalNodeFull(createFolder, this.#keys);
297
+ const folder = await apiNodeToExternalNodeFull(createdFolder, this.#keys);
296
298
  const users = folder.parent?.users?.filter(([u]) => u.id !== this.#client.app.userId) ??
297
299
  [];
298
300
  if (users.length > 0) {
299
- await Promise.all(users.map(async ([u, rights]) => await this.shareNode({
300
- rights,
301
- nodes: [
302
- {
303
- userId: u.id,
304
- nodeId: folder.id,
305
- },
306
- ],
307
- })));
301
+ await this.shareNode({
302
+ nodeId: folder.id,
303
+ users: users.map(([user, rights]) => ({ id: user.id, rights })),
304
+ });
308
305
  }
309
306
  return folder;
310
307
  }
@@ -322,23 +319,33 @@ export class SecrecyCloudClient {
322
319
  return apiDataToExternal(data, this.#keys);
323
320
  }
324
321
  async shareNode(input) {
325
- const userIds = Array.isArray(input.nodes)
326
- ? input.nodes.map(({ userId }) => userId)
327
- : 'userIds' in input.nodes
328
- ? input.nodes.userIds
329
- : [input.nodes.userId];
322
+ const userIds = 'rights' in input
323
+ ? Array.isArray(input.nodes)
324
+ ? input.nodes.map(({ userId }) => userId)
325
+ : 'userIds' in input.nodes
326
+ ? input.nodes.userIds
327
+ : [input.nodes.userId]
328
+ : 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
+ };
330
335
  const [publicKeysMap, nodesIdsMap] = await Promise.all([
331
336
  this.#client.app.userPublicKey(userIds),
332
- this.#apiClient.cloud.shareNode.mutate(input.nodes),
337
+ this.#apiClient.cloud.shareNode.mutate(nodesToShare),
333
338
  ]);
334
- const nodesInfos = await this.perNodes(nodesIdsMap, publicKeysMap);
335
- const sharingState = await this.#apiClient.cloud.shareNodeFinish.mutate(Object.entries(nodesInfos).map(([userId, nodes]) => {
336
- return {
337
- userId,
338
- nodes,
339
- rights: input.rights,
340
- };
341
- }));
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
+ }));
342
349
  return sharingState;
343
350
  }
344
351
  async updateNode({ nodeId, name, isFavorite, deletedAt, }) {
@@ -509,9 +516,11 @@ export class SecrecyCloudClient {
509
516
  key = key
510
517
  ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(key), this.#keys.publicKey, this.#keys.privateKey))
511
518
  : null;
512
- const nameKey = secretStreamKeygen();
513
- const encryptedName = await encryptName(name, sodium.to_hex(nameKey));
514
- const encryptedNameKey = sodium.to_hex(encryptCryptoBox(nameKey, this.#keys.publicKey, this.#keys.privateKey));
519
+ const { encryptedNameKey, encryptedName } = await generateAndEncryptNameAndKey({
520
+ name,
521
+ privateKey: this.#keys.privateKey,
522
+ publicKey: this.#keys.publicKey,
523
+ });
515
524
  const saveInCloud = await this.#apiClient.cloud.saveInCloud.mutate({
516
525
  dataId,
517
526
  key,
@@ -524,19 +533,16 @@ export class SecrecyCloudClient {
524
533
  if (me !== undefined && ['admin', 'write'].includes(me[1])) {
525
534
  const others = node.parent?.users.filter(([u]) => u.id !== this.#client.app.userId) ??
526
535
  [];
527
- await Promise.all(others.map(async ([u, rights]) => await this.shareNode({
528
- rights,
529
- nodes: [
530
- {
531
- nodeId: node.id,
532
- userId: u.id,
533
- },
534
- ],
535
- })));
536
+ if (others.length > 0) {
537
+ await this.shareNode({
538
+ nodeId: node.id,
539
+ users: others.map(([user, rights]) => ({ id: user.id, rights })),
540
+ });
541
+ }
536
542
  }
537
543
  return node;
538
544
  }
539
- perNodes = async (userNodes, // { [userId]: nodeIds[] }
545
+ encryptNodesForUsers = async (userNodes, // { [userId]: nodeIds[] }
540
546
  userPublicKeys) => {
541
547
  const userIds = Object.keys(userNodes).map((userId) => userId);
542
548
  const nodeIds = Object.values(userNodes).flatMap((nodeIds) => nodeIds);
@@ -579,18 +585,10 @@ export class SecrecyCloudClient {
579
585
  const diff = missingNodeIds.filter((id) => !fetchedNodes.some((node) => node.id === id));
580
586
  throw new Error(`Unable to fetch some node infos (${diff.join(', ')})`);
581
587
  }
582
- nodes.push(...fetchedNodes.map((node) => {
583
- return {
584
- id: node.id,
585
- type: node.type,
586
- name: node.name,
587
- access: { nameKey: node.access.nameKey },
588
- history: node.history.map((data) => ({
589
- id: data.id,
590
- key: data.access.key,
591
- })),
592
- };
588
+ const finalNodes = await Promise.all(fetchedNodes.map((node) => {
589
+ return apiNodeForEncryptionToInternal(node, this.#keys);
593
590
  }));
591
+ nodes.push(...finalNodes);
594
592
  const nodesMappedUsers = {};
595
593
  for (const userId in userNodes) {
596
594
  nodesMappedUsers[userId] ??= [];
@@ -605,7 +603,6 @@ export class SecrecyCloudClient {
605
603
  }
606
604
  const nameKey = node.access?.nameKey;
607
605
  const publicKey = userPublicKeys[userId];
608
- nodesEncryptionCache.set(node.id, node);
609
606
  nodesMappedUsers[userId].push({
610
607
  id: node.id,
611
608
  nameKey: nameKey !== null
@@ -1,17 +1,8 @@
1
1
  import { sodium } from '../../sodium.js';
2
2
  import { decryptCryptoBox } from '../../crypto/index.js';
3
- import { nodesCache } from '../../cache.js';
3
+ import { nodesCache, nodesEncryptionCache } from '../../cache.js';
4
4
  import { decryptSecretStream } from '../../crypto/data.js';
5
5
  import { apiDataToInternal, internalDataToExternalData } from './data.js';
6
- async function readName({ encryptedName, encryptedNameKey, sharedByPubKey, privateKey, }) {
7
- const nameKeyBuffer = decryptCryptoBox(sodium.from_hex(encryptedNameKey), sharedByPubKey, privateKey);
8
- const nameKey = sodium.to_hex(nameKeyBuffer);
9
- const name = sodium.to_string(await decryptSecretStream(nameKeyBuffer, sodium.from_hex(encryptedName)));
10
- return {
11
- name,
12
- nameKey,
13
- };
14
- }
15
6
  async function apiNodeToInternal(apiNode, keyPair) {
16
7
  const internal = {
17
8
  id: apiNode.id,
@@ -34,6 +25,7 @@ async function apiNodeToInternal(apiNode, keyPair) {
34
25
  parentId: apiNode.parentId ?? null,
35
26
  currentDataId: apiNode.currentDataId ?? null,
36
27
  };
28
+ internal.access = { ...apiNode.access };
37
29
  if (apiNode.access.nameKey !== null) {
38
30
  const key = decryptCryptoBox(sodium.from_hex(apiNode.access.nameKey), apiNode.access.sharedByPubKey, keyPair.privateKey);
39
31
  internal.name = sodium.to_string(await decryptSecretStream(key, sodium.from_hex(internal.name)));
@@ -54,7 +46,7 @@ export async function apiNodeFullToInternalFull(apiNodeFull, keyPair) {
54
46
  const f = await apiNodeToInternal(apiNodeFull, keyPair);
55
47
  const nodeFull = {
56
48
  ...f,
57
- current: apiNodeFull.current !== null
49
+ current: !!apiNodeFull.current
58
50
  ? apiDataToInternal(apiNodeFull.current, keyPair)
59
51
  : undefined,
60
52
  parent: apiNodeFull.parent !== null
@@ -100,3 +92,27 @@ export async function apiNodeToExternal(apiNode, keyPair) {
100
92
  const internal = await apiNodeToInternal(apiNode, keyPair);
101
93
  return internalNodeToNode(internal);
102
94
  }
95
+ export async function apiNodeForEncryptionToInternal(apiNode, keyPair) {
96
+ const history = apiNode.history.map((history) => {
97
+ const key = decryptCryptoBox(sodium.from_hex(history.access.key), history.access.sharedByPublicKey, keyPair.privateKey);
98
+ return {
99
+ id: history.id,
100
+ key: sodium.to_hex(key),
101
+ };
102
+ });
103
+ const internal = {
104
+ id: apiNode.id,
105
+ type: apiNode.type,
106
+ access: apiNode.access,
107
+ name: apiNode.name,
108
+ history: history,
109
+ };
110
+ internal.access = { ...apiNode.access };
111
+ if (apiNode.access.nameKey !== null) {
112
+ const key = decryptCryptoBox(sodium.from_hex(apiNode.access.nameKey), apiNode.access.sharedByPublicKey, keyPair.privateKey);
113
+ internal.name = sodium.to_string(await decryptSecretStream(key, sodium.from_hex(internal.name)));
114
+ internal.access.nameKey = sodium.to_hex(key);
115
+ }
116
+ nodesEncryptionCache.set(apiNode.id, internal);
117
+ return internal;
118
+ }
@@ -1,6 +1,4 @@
1
1
  import { BaseClient } from '../base-client.js';
2
- import { encryptSecretStream } from '../crypto/data.js';
3
- import { sodium } from '../sodium.js';
4
2
  import { SecrecyCloudClient } from './SecrecyCloudClient.js';
5
3
  import { SecrecyMailClient } from './SecrecyMailClient.js';
6
4
  import { SecrecyAppClient } from './SecrecyAppClient.js';
@@ -11,11 +9,6 @@ import { SecrecyPayClient } from './SecrecyPayClient.js';
11
9
  import { SecrecyUserClient } from './SecrecyUserClient.js';
12
10
  import { SecrecyPseudonymClient } from './SecrecyPseudonymClient.js';
13
11
  import { decryptAnonymous } from '../crypto/index.js';
14
- export const encryptName = async (name, nameKey) => {
15
- const { data } = await encryptSecretStream(sodium.from_hex(nameKey), sodium.from_string(name));
16
- const nameEncrypted = sodium.to_hex(data);
17
- return nameEncrypted;
18
- };
19
12
  export class SecrecyClient extends BaseClient {
20
13
  #keys;
21
14
  cloud;
@@ -30,6 +23,7 @@ export class SecrecyClient extends BaseClient {
30
23
  super({
31
24
  session: opts.uaSession,
32
25
  secrecyUrls: opts.secrecyUrls,
26
+ apiClient: opts.apiClient,
33
27
  onAccessDenied: async () => {
34
28
  console.log('[CLIENT] - Access denied');
35
29
  try {
@@ -0,0 +1,17 @@
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);
7
+ }
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));
12
+ return {
13
+ nameKey,
14
+ encryptedName,
15
+ encryptedNameKey,
16
+ };
17
+ }
@@ -1,18 +1,33 @@
1
+ import SparkMD5 from 'spark-md5';
1
2
  import { workerMd5Script } from './workerCodes.js';
2
- export async function md5(data) {
3
- return await new Promise((resolve, reject) => {
4
- const worker = new Worker(URL.createObjectURL(new Blob([workerMd5Script], { type: 'text/javascript' })));
5
- worker.addEventListener('error', reject);
6
- worker.addEventListener('messageerror', reject);
7
- worker.addEventListener('message', ({ data }) => {
8
- if (data.event === 'md5-result') {
9
- worker.terminate();
10
- resolve(data.data);
11
- }
12
- });
13
- worker.postMessage({
14
- event: 'md5',
15
- data,
16
- });
17
- });
3
+ function* chunks(arr, n) {
4
+ for (let i = 0; i < arr.length; i += n) {
5
+ yield arr.slice(i, i + n);
6
+ }
18
7
  }
8
+ const CHUNK_SIZE = 8192;
9
+ export const md5 = process.env.NODE_ENV !== 'test'
10
+ ? async (data) => {
11
+ return await new Promise((resolve, reject) => {
12
+ const worker = new Worker(URL.createObjectURL(new Blob([workerMd5Script], { type: 'text/javascript' })));
13
+ worker.addEventListener('error', reject);
14
+ worker.addEventListener('messageerror', reject);
15
+ worker.addEventListener('message', ({ data }) => {
16
+ if (data.event === 'md5-result') {
17
+ worker.terminate();
18
+ resolve(data.data);
19
+ }
20
+ });
21
+ worker.postMessage({
22
+ event: 'md5',
23
+ data,
24
+ });
25
+ });
26
+ }
27
+ : async (data) => {
28
+ const spark = new SparkMD5.ArrayBuffer();
29
+ for (const chunk of chunks(data, CHUNK_SIZE)) {
30
+ spark.append(chunk.buffer);
31
+ }
32
+ return spark.end();
33
+ };
@@ -1,3 +1,4 @@
1
+ import { decryptSecretStream, encryptSecretStream, } from '../crypto/data.js';
1
2
  import { workerSodiumScript } from './workerCodes.js';
2
3
  // const ensureNonDetachedUniqueBuffers = (
3
4
  // values: Transferable[]
@@ -11,85 +12,93 @@ import { workerSodiumScript } from './workerCodes.js';
11
12
  // }
12
13
  // })
13
14
  // );
14
- export async function encrypt(key, dataToEncrypt, progress, signal) {
15
- return await new Promise((resolve, reject) => {
16
- void progress?.({
17
- current: 0,
18
- percent: 0,
19
- total: dataToEncrypt.byteLength,
20
- });
21
- const worker = new Worker(URL.createObjectURL(new Blob([workerSodiumScript], { type: 'text/javascript' })));
22
- worker.addEventListener('error', reject);
23
- worker.addEventListener('messageerror', reject);
24
- worker.addEventListener('message', ({ data }) => {
25
- switch (data.event) {
26
- case 'ready': {
27
- const postData = {
28
- event: 'encrypt',
29
- data: dataToEncrypt,
30
- key,
31
- };
32
- worker.postMessage(postData, {
33
- transfer: [postData.data.buffer],
34
- });
35
- break;
36
- }
37
- case 'encrypt-progress': {
38
- if (signal?.aborted === true) {
39
- const abortError = new Error('Aborted');
40
- abortError.name = 'AbortError';
15
+ export const encrypt = process.env.NODE_ENV !== 'test'
16
+ ? async (key, dataToEncrypt, progress, signal) => {
17
+ return await new Promise((resolve, reject) => {
18
+ void progress?.({
19
+ current: 0,
20
+ percent: 0,
21
+ total: dataToEncrypt.byteLength,
22
+ });
23
+ const worker = new Worker(URL.createObjectURL(new Blob([workerSodiumScript], { type: 'text/javascript' })));
24
+ worker.addEventListener('error', reject);
25
+ worker.addEventListener('messageerror', reject);
26
+ worker.addEventListener('message', ({ data }) => {
27
+ switch (data.event) {
28
+ case 'ready': {
29
+ const postData = {
30
+ event: 'encrypt',
31
+ data: dataToEncrypt,
32
+ key,
33
+ };
34
+ worker.postMessage(postData, {
35
+ transfer: [postData.data.buffer],
36
+ });
37
+ break;
38
+ }
39
+ case 'encrypt-progress': {
40
+ if (signal?.aborted === true) {
41
+ const abortError = new Error('Aborted');
42
+ abortError.name = 'AbortError';
43
+ worker.terminate();
44
+ reject(abortError);
45
+ }
46
+ void progress?.(data.data);
47
+ break;
48
+ }
49
+ case 'encrypt-result': {
41
50
  worker.terminate();
42
- reject(abortError);
51
+ resolve(data.data);
43
52
  }
44
- void progress?.(data.data);
45
- break;
46
- }
47
- case 'encrypt-result': {
48
- worker.terminate();
49
- resolve(data.data);
50
53
  }
51
- }
54
+ });
52
55
  });
53
- });
54
- }
55
- export async function decrypt(key, dataToDecrypt, progress, signal) {
56
- return await new Promise((resolve, reject) => {
57
- void progress?.({
58
- current: 0,
59
- percent: 0,
60
- total: dataToDecrypt.byteLength,
61
- });
62
- const worker = new Worker(URL.createObjectURL(new Blob([workerSodiumScript], { type: 'text/javascript' })));
63
- worker.addEventListener('error', reject);
64
- worker.addEventListener('messageerror', reject);
65
- worker.addEventListener('message', ({ data }) => {
66
- switch (data.event) {
67
- case 'ready': {
68
- const postData = {
69
- event: 'decrypt',
70
- key,
71
- data: dataToDecrypt,
72
- };
73
- worker.postMessage(postData, {
74
- transfer: [postData.data.buffer],
75
- });
76
- break;
77
- }
78
- case 'decrypt-progress': {
79
- if (signal?.aborted === true) {
80
- const abortError = new Error('Aborted');
81
- abortError.name = 'AbortError';
56
+ }
57
+ : async (key, dataToEncrypt, progress, signal) => {
58
+ return encryptSecretStream(key, dataToEncrypt);
59
+ };
60
+ export const decrypt = process.env.NODE_ENV !== 'test'
61
+ ? async (key, dataToDecrypt, progress, signal) => {
62
+ return await new Promise((resolve, reject) => {
63
+ void progress?.({
64
+ current: 0,
65
+ percent: 0,
66
+ total: dataToDecrypt.byteLength,
67
+ });
68
+ const worker = new Worker(URL.createObjectURL(new Blob([workerSodiumScript], { type: 'text/javascript' })));
69
+ worker.addEventListener('error', reject);
70
+ worker.addEventListener('messageerror', reject);
71
+ worker.addEventListener('message', ({ data }) => {
72
+ switch (data.event) {
73
+ case 'ready': {
74
+ const postData = {
75
+ event: 'decrypt',
76
+ key,
77
+ data: dataToDecrypt,
78
+ };
79
+ worker.postMessage(postData, {
80
+ transfer: [postData.data.buffer],
81
+ });
82
+ break;
83
+ }
84
+ case 'decrypt-progress': {
85
+ if (signal?.aborted === true) {
86
+ const abortError = new Error('Aborted');
87
+ abortError.name = 'AbortError';
88
+ worker.terminate();
89
+ reject(abortError);
90
+ }
91
+ void progress?.(data.data);
92
+ break;
93
+ }
94
+ case 'decrypt-result': {
82
95
  worker.terminate();
83
- reject(abortError);
96
+ resolve(data.data);
84
97
  }
85
- void progress?.(data.data);
86
- break;
87
- }
88
- case 'decrypt-result': {
89
- worker.terminate();
90
- resolve(data.data);
91
98
  }
92
- }
99
+ });
93
100
  });
94
- });
95
- }
101
+ }
102
+ : async (key, dataToDecrypt, progress, signal) => {
103
+ return decryptSecretStream(key, dataToDecrypt);
104
+ };
@@ -0,0 +1,140 @@
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
+ }
@@ -8,6 +8,7 @@ export type SecrecyUrls = {
8
8
  };
9
9
  export type BaseClientOptions = {
10
10
  session: string;
11
+ apiClient?: ApiClient;
11
12
  onAccessDenied?: () => void | Promise<void>;
12
13
  secrecyUrls?: Partial<SecrecyUrls>;
13
14
  };
@@ -56,7 +56,25 @@ export declare class SecrecyCloudClient {
56
56
  }): Promise<DataMetadata>;
57
57
  shareNode(input: {
58
58
  rights: Rights;
59
- nodes: RouterInputs['cloud']['shareNode'];
59
+ nodes: {
60
+ nodeId: string;
61
+ userId: string;
62
+ }[] | {
63
+ nodeId: string;
64
+ userIds: string[];
65
+ } | {
66
+ nodeIds: string[];
67
+ userId: string;
68
+ } | {
69
+ nodeIds: string[];
70
+ userIds: string[];
71
+ };
72
+ } | {
73
+ nodeId: string;
74
+ users: {
75
+ id: string;
76
+ rights: Rights;
77
+ }[];
60
78
  }): Promise<RouterOutputs['cloud']['shareNodeFinish']>;
61
79
  updateNode({ nodeId, name, isFavorite, deletedAt, }: {
62
80
  nodeId: string;
@@ -99,7 +117,7 @@ export declare class SecrecyCloudClient {
99
117
  name: string;
100
118
  nodeId?: string;
101
119
  }): Promise<NodeFull>;
102
- private readonly perNodes;
120
+ private readonly encryptNodesForUsers;
103
121
  reportData({ id, reasons, }: Omit<RouterInputs['cloud']['reportData'], 'encryptedDataKey'>): Promise<RouterOutputs['cloud']['reportData']>;
104
122
  updateDataStorageType(input: RouterInputs['cloud']['moveToStorageType']): Promise<{
105
123
  isMoved: boolean;
@@ -1,5 +1,6 @@
1
- import type { Node, ApiNode, ApiNodeFull, InternalNodeFull, NodeFull, KeyPair, ApiNodeParent } from '../types/index.js';
1
+ import type { Node, ApiNode, ApiNodeFull, InternalNodeFull, NodeFull, KeyPair, ApiNodeParent, ApiNodeForEncryption, InternalMinimalNodeForEncryption } from '../types/index.js';
2
2
  export declare function apiNodeFullToInternalFull(apiNodeFull: ApiNodeFull, keyPair: KeyPair): Promise<InternalNodeFull>;
3
3
  export declare function internalNodeFullToNodeFull(internal: InternalNodeFull): NodeFull;
4
4
  export declare function apiNodeToExternalNodeFull(apiNodeFull: ApiNodeFull, keyPair: KeyPair): Promise<NodeFull>;
5
5
  export declare function apiNodeToExternal(apiNode: ApiNode | ApiNodeParent, keyPair: KeyPair): Promise<Node>;
6
+ export declare function apiNodeForEncryptionToInternal(apiNode: ApiNodeForEncryption, keyPair: KeyPair): Promise<InternalMinimalNodeForEncryption>;
@@ -6,17 +6,17 @@ import { SecrecyAppClient } from './SecrecyAppClient.js';
6
6
  import { SecrecyDbClient } from './SecrecyDbClient.js';
7
7
  import { SecrecyWalletClient } from './SecrecyWalletClient.js';
8
8
  import { SecrecyPayClient } from './SecrecyPayClient.js';
9
- import { type RouterInputs } from '../client.js';
9
+ import { ApiClient, type RouterInputs } from '../client.js';
10
10
  import { type KeyPair } from './types/index.js';
11
11
  import { SecrecyUserClient } from './SecrecyUserClient.js';
12
12
  import { SecrecyPseudonymClient } from './SecrecyPseudonymClient.js';
13
13
  export type NewMail = Pick<RouterInputs['mail']['createDraft'], 'body' | 'subject' | 'senderFiles' | 'recipients' | 'replyToId'>;
14
14
  export type ProgressCallback = (progress: Progress) => Promise<void>;
15
- export declare const encryptName: (name: string, nameKey: string) => Promise<string>;
16
15
  export interface SecrecyClientOptions {
17
16
  uaSession: string;
18
17
  uaKeys: KeyPair;
19
18
  uaJwt: string;
19
+ apiClient?: ApiClient;
20
20
  secrecyUrls?: Partial<SecrecyUrls>;
21
21
  }
22
22
  export declare class SecrecyClient extends BaseClient {
@@ -58,7 +58,16 @@ export type InternalMinimalNodeForEncryption = {
58
58
  export type InternalNode = Node<InternalNodeBreadcrumbItem, NameKey>;
59
59
  export type InternalNodeFull = NodeFull<InternalNodeBreadcrumbItem, NameKey, InternalData>;
60
60
  export type ApiNode = RouterOutputs['cloud']['nodeById'];
61
+ export type ApiNodeForEncryption = RouterOutputs['cloud']['nodesForEncryption'][number];
61
62
  export type ApiNodeFull = RouterOutputs['cloud']['nodeFullById'];
62
63
  export type ApiNodeParent = NonNullable<RouterOutputs['cloud']['nodeFullById']['parent']>;
63
64
  export type NodeType = ApiNode['type'];
65
+ export type EncryptedNodeInfos = {
66
+ id: string;
67
+ nameKey: string | null;
68
+ data: {
69
+ id: string;
70
+ key: string | null;
71
+ }[];
72
+ };
64
73
  export {};
@@ -5392,10 +5392,12 @@ export declare const createTRPCClient: (opts: CreateTrpcClientOptions) => {
5392
5392
  id: string;
5393
5393
  access: {
5394
5394
  key: string;
5395
+ sharedByPublicKey: string;
5395
5396
  };
5396
5397
  }[];
5397
5398
  access: {
5398
5399
  nameKey: string;
5400
+ sharedByPublicKey: string;
5399
5401
  };
5400
5402
  }[];
5401
5403
  _output_out: {
@@ -5406,10 +5408,12 @@ export declare const createTRPCClient: (opts: CreateTrpcClientOptions) => {
5406
5408
  id: string;
5407
5409
  access: {
5408
5410
  key: string;
5411
+ sharedByPublicKey: string;
5409
5412
  };
5410
5413
  }[];
5411
5414
  access: {
5412
5415
  nameKey: string;
5416
+ sharedByPublicKey: string;
5413
5417
  };
5414
5418
  }[];
5415
5419
  }, unknown>>;
@@ -0,0 +1,10 @@
1
+ export declare function encryptName(name: string, nameKey: string): Promise<string>;
2
+ export declare function generateAndEncryptNameAndKey(args: {
3
+ name: string;
4
+ privateKey: string;
5
+ publicKey: string;
6
+ }): Promise<{
7
+ nameKey: Uint8Array<ArrayBufferLike>;
8
+ encryptedName: string;
9
+ encryptedNameKey: string;
10
+ }>;
@@ -1 +1 @@
1
- export declare function md5(data: Uint8Array): Promise<string>;
1
+ export declare const md5: (data: Uint8Array) => Promise<string>;
@@ -1,3 +1,3 @@
1
- import type { EncryptedFile, Progress } from '../crypto/data.js';
2
- export declare function encrypt(key: Uint8Array, dataToEncrypt: Uint8Array, progress?: (progress: Progress) => Promise<void>, signal?: AbortSignal): Promise<EncryptedFile>;
3
- export declare function decrypt(key: Uint8Array, dataToDecrypt: Uint8Array, progress?: (progress: Progress) => Promise<void>, signal?: AbortSignal): Promise<Uint8Array>;
1
+ import { type EncryptedFile, type Progress } from '../crypto/data.js';
2
+ export declare const encrypt: (key: Uint8Array, dataToEncrypt: Uint8Array, progress?: (progress: Progress) => Promise<void>, signal?: AbortSignal) => Promise<EncryptedFile>;
3
+ export declare const decrypt: (key: Uint8Array, dataToDecrypt: Uint8Array, progress?: (progress: Progress) => Promise<void>, signal?: AbortSignal) => Promise<Uint8Array>;
@@ -0,0 +1,6 @@
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>>;
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.1",
5
+ "version": "1.62.0-feat-node-sharing.3",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/anonymize-org/lib.git"
@@ -74,7 +74,7 @@
74
74
  "typescript": "^5.7.2"
75
75
  },
76
76
  "dependencies": {
77
- "@secrecy/trpc-api-types": "1.33.0-feat-share-node-enhanced.11",
77
+ "@secrecy/trpc-api-types": "1.33.0-feat-share-node-enhanced.12",
78
78
  "@trpc/client": "10.45.2",
79
79
  "@trpc/server": "10.45.2",
80
80
  "@types/libsodium-wrappers-sumo": "^0.7.8",