@secrecy/lib 1.86.2 → 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.
Files changed (47) hide show
  1. package/dist/lib/base-client.js +6 -5
  2. package/dist/lib/cache.js +14 -5
  3. package/dist/lib/client/SecrecyCloudClient.js +4 -2
  4. package/dist/lib/client/SecrecyMailClient.js +12 -5
  5. package/dist/lib/client/data-link.js +6 -12
  6. package/dist/lib/client/helpers.js +21 -1
  7. package/dist/lib/client/index.js +6 -8
  8. package/dist/lib/client/upload.js +12 -14
  9. package/dist/lib/client.js +6 -10
  10. package/dist/lib/crypto/data.js +146 -91
  11. package/dist/lib/crypto/domain.js +1 -3
  12. package/dist/lib/crypto/helpers.js +23 -1
  13. package/dist/lib/crypto/index.js +2 -2
  14. package/dist/lib/error/client.js +7 -1
  15. package/dist/lib/react/index.js +69 -0
  16. package/dist/lib/transformer.js +3 -2
  17. package/dist/lib/utils/popup-tools.js +31 -13
  18. package/dist/lib/versioning.js +1 -1
  19. package/dist/lib/worker/generated/md5.js +2 -0
  20. package/dist/lib/worker/generated/sodium.js +2 -0
  21. package/dist/lib/worker/md5.js +2 -2
  22. package/dist/lib/worker/sodium.js +17 -15
  23. package/dist/types/base-client.d.ts +2 -0
  24. package/dist/types/cache.d.ts +7 -6
  25. package/dist/types/client/SecrecyMailClient.d.ts +23 -1
  26. package/dist/types/client/data-link.d.ts +4 -4
  27. package/dist/types/client/helpers.d.ts +12 -0
  28. package/dist/types/client/index.d.ts +4 -2
  29. package/dist/types/client/types/data.d.ts +2 -1
  30. package/dist/types/client.d.ts +67 -74
  31. package/dist/types/crypto/data.d.ts +11 -1
  32. package/dist/types/crypto/domain.d.ts +0 -1
  33. package/dist/types/crypto/helpers.d.ts +7 -0
  34. package/dist/types/crypto/index.d.ts +3 -3
  35. package/dist/types/error/client.d.ts +8 -0
  36. package/dist/types/error/index.d.ts +4 -0
  37. package/dist/types/react/index.d.ts +20 -0
  38. package/dist/types/sodium.d.ts +5 -0
  39. package/dist/types/utils/popup-tools.d.ts +4 -2
  40. package/dist/types/versioning.d.ts +1 -1
  41. package/dist/types/worker/generated/md5.d.ts +1 -0
  42. package/dist/types/worker/generated/sodium.d.ts +1 -0
  43. package/package.json +37 -13
  44. package/dist/lib/client/SecrecyDbClient.js +0 -3
  45. package/dist/lib/worker/workerCodes.js +0 -254
  46. package/dist/types/client/SecrecyDbClient.d.ts +0 -4
  47. package/dist/types/worker/workerCodes.d.ts +0 -2
@@ -1,4 +1,4 @@
1
- import { usersCache } from './cache.js';
1
+ import { usersCache, clearAllCaches } from './cache.js';
2
2
  import { getStorage } from './client/storage.js';
3
3
  import { createTRPCClient, } from './client.js';
4
4
  import { getPreferedEmail } from './utils.js';
@@ -46,15 +46,16 @@ export class BaseClient {
46
46
  BaseClient.getBaseClient({
47
47
  session: opts.session,
48
48
  apiUrl: this.secrecyUrls.api,
49
+ timeoutMs: opts.timeoutMs,
49
50
  onAccessDenied: async () => {
50
- console.log('[BASE_CLIENT - BEFORE] - Access denied');
51
51
  await opts.onAccessDenied?.();
52
- console.log('[BASE_CLIENT - AFTER] - Access denied');
53
52
  try {
54
53
  await this.logout();
55
54
  }
56
55
  finally {
57
- location.reload();
56
+ if (opts.reloadOnAccessDenied ?? true) {
57
+ location.reload();
58
+ }
58
59
  }
59
60
  },
60
61
  });
@@ -141,6 +142,6 @@ export class BaseClient {
141
142
  session.identities.clear();
142
143
  session.keyPairs.clear();
143
144
  session.userAppSession.clear();
144
- usersCache.clear();
145
+ clearAllCaches();
145
146
  };
146
147
  }
package/dist/lib/cache.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { LRUCache } from 'lru-cache';
2
2
  import { gigaToBytes } from './utils.js';
3
- export const dataCache = new Map();
4
- export const nodesCache = new Map();
5
- export const nodesEncryptionCache = new Map();
3
+ const boundedCacheOpts = { max: 5_000, ttl: 5 * 60 * 1000 };
4
+ export const dataCache = new LRUCache(boundedCacheOpts);
5
+ export const nodesCache = new LRUCache(boundedCacheOpts);
6
+ export const nodesEncryptionCache = new LRUCache(boundedCacheOpts);
6
7
  export const getNodeForEncryptionFromCache = (id) => {
7
8
  if (nodesEncryptionCache.has(id)) {
8
9
  return nodesEncryptionCache.get(id);
@@ -12,8 +13,8 @@ export const getNodeForEncryptionFromCache = (id) => {
12
13
  }
13
14
  return undefined;
14
15
  };
15
- export const usersCache = new Map();
16
- export const publicKeysCache = new Map();
16
+ export const usersCache = new LRUCache(boundedCacheOpts);
17
+ export const publicKeysCache = new LRUCache({ max: 10_000 });
17
18
  export const dataContentCache = new LRUCache({
18
19
  max: 500,
19
20
  maxSize: gigaToBytes(0.5),
@@ -21,3 +22,11 @@ export const dataContentCache = new LRUCache({
21
22
  return value.data.byteLength;
22
23
  },
23
24
  });
25
+ export function clearAllCaches() {
26
+ dataCache.clear();
27
+ nodesCache.clear();
28
+ nodesEncryptionCache.clear();
29
+ usersCache.clear();
30
+ publicKeysCache.clear();
31
+ dataContentCache.clear();
32
+ }
@@ -702,8 +702,10 @@ export class SecrecyCloudClient {
702
702
  const src = key
703
703
  ? await decrypt(key, encryptedContent, progressDecrypt, signal)
704
704
  : encryptedContent;
705
- const md5Content = await md5(src);
706
- if (md5Content !== dataContent.md5) {
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);
@@ -31,8 +31,13 @@ export class SecrecyMailClient {
31
31
  })));
32
32
  }
33
33
  async create(data, customMessage) {
34
- const mail = await this.createDraft(data);
35
- const isSent = await this.sendDraft(mail.mailIntegrityId, customMessage);
34
+ const { customMessage: embeddedCustomMessage, ...mailData } = data;
35
+ const resolvedCustomMessage = customMessage ?? embeddedCustomMessage;
36
+ const mail = await this.createDraft(mailData);
37
+ const isSent = await this.sendDraft({
38
+ draftId: mail.mailIntegrityId,
39
+ customMessage: resolvedCustomMessage,
40
+ });
36
41
  if (!isSent) {
37
42
  throw new Error('The mail does not sent!');
38
43
  }
@@ -53,7 +58,8 @@ export class SecrecyMailClient {
53
58
  }));
54
59
  return waitingReceivedMailsWithIds;
55
60
  }
56
- async updateDraft(draftId, { body, subject, senderFiles, recipients, replyToId }) {
61
+ async updateDraft(arg, args) {
62
+ const { draftId, body, subject, senderFiles, recipients, replyToId, } = typeof arg === 'string' ? { draftId: arg, ...args } : arg;
57
63
  const drafts = await this.draftMails({});
58
64
  const draft = drafts.find((d) => d.mailIntegrityId === draftId);
59
65
  if (draft === undefined) {
@@ -124,7 +130,8 @@ export class SecrecyMailClient {
124
130
  });
125
131
  return isDeleted;
126
132
  }
127
- async sendDraft(draftId, customMessage) {
133
+ async sendDraft(arg, customMessage) {
134
+ const { draftId, customMessage: resolvedCustomMessage } = typeof arg === 'string' ? { draftId: arg, customMessage } : arg;
128
135
  const drafts = await this.draftMails({});
129
136
  const draft = drafts.find((d) => d.mailIntegrityId === draftId);
130
137
  if (draft === undefined) {
@@ -157,7 +164,7 @@ export class SecrecyMailClient {
157
164
  temporaryRecipients,
158
165
  recipients,
159
166
  id: draft.mailIntegrityId,
160
- customMessage: customMessage ?? null,
167
+ customMessage: resolvedCustomMessage ?? null,
161
168
  });
162
169
  return isSent;
163
170
  }
@@ -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 { derivePassword } from '../crypto/helpers';
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
- md5: z.string(),
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
- md5: z.string(),
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 salt = sodium.crypto_generichash(sodium.crypto_pwhash_SALTBYTES, opts.crypto.password + dataLink.md5);
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) {
@@ -2,6 +2,7 @@ import { SecrecyClient } from './index.js';
2
2
  import { popup } from '../utils/popup-tools.js';
3
3
  import { secrecyUserApp } from './types/index.js';
4
4
  import { getStorage } from './storage.js';
5
+ import { setup } from '../sodium.js';
5
6
  export function parseInfos() {
6
7
  if (window.location.hash === '') {
7
8
  return null;
@@ -9,7 +10,9 @@ export function parseInfos() {
9
10
  const hash = window.location.hash.substr(1);
10
11
  try {
11
12
  const res = JSON.parse(atob(hash));
12
- window.location.hash = '';
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);
13
16
  const data = secrecyUserApp.safeParse(res);
14
17
  return data.success ? data.data : null;
15
18
  }
@@ -129,3 +132,20 @@ export async function login({ appId, context, path, redirect, scopes, backPath,
129
132
  }
130
133
  });
131
134
  }
135
+ /**
136
+ * Single async entry point: runs `setup()`, reuses an existing session if
137
+ * one is found, and falls back to `login()` otherwise. Equivalent to
138
+ * `await setup()` + `getSecrecyClient()` + `login()`, without having to
139
+ * juggle the sync/async mismatch between them.
140
+ */
141
+ export async function initSecrecy(opts) {
142
+ await setup();
143
+ const existing = getSecrecyClient({
144
+ session: opts.session,
145
+ secrecyUrls: opts.secrecyUrls,
146
+ });
147
+ if (existing !== null) {
148
+ return existing;
149
+ }
150
+ return await login(opts);
151
+ }
@@ -2,8 +2,7 @@ import { BaseClient } from '../base-client.js';
2
2
  import { SecrecyCloudClient } from './SecrecyCloudClient.js';
3
3
  import { SecrecyMailClient } from './SecrecyMailClient.js';
4
4
  import { SecrecyAppClient } from './SecrecyAppClient.js';
5
- import { nodesCache, dataCache, publicKeysCache } from '../cache.js';
6
- import { SecrecyDbClient } from './SecrecyDbClient.js';
5
+ import { clearAllCaches } from '../cache.js';
7
6
  import { SecrecyWalletClient } from './SecrecyWalletClient.js';
8
7
  import { SecrecyPayClient } from './SecrecyPayClient.js';
9
8
  import { SecrecyUserClient } from './SecrecyUserClient.js';
@@ -20,9 +19,10 @@ export class SecrecyClient extends BaseClient {
20
19
  cloud;
21
20
  mail;
22
21
  app;
23
- db;
24
22
  organization;
23
+ /** @experimental Susceptible de changer sans version majeure. */
25
24
  wallet;
25
+ /** @experimental Susceptible de changer sans version majeure. */
26
26
  pay;
27
27
  user;
28
28
  pseudonym;
@@ -33,8 +33,9 @@ export class SecrecyClient extends BaseClient {
33
33
  session: opts.uaSession,
34
34
  secrecyUrls: opts.secrecyUrls,
35
35
  apiClient: opts.apiClient,
36
+ timeoutMs: opts.timeoutMs,
37
+ reloadOnAccessDenied: opts.reloadOnAccessDenied,
36
38
  onAccessDenied: async () => {
37
- console.log('[CLIENT] - Access denied');
38
39
  try {
39
40
  await this.logout();
40
41
  }
@@ -53,7 +54,6 @@ export class SecrecyClient extends BaseClient {
53
54
  this.cloud = new SecrecyCloudClient(this);
54
55
  this.mail = new SecrecyMailClient(this);
55
56
  this.app = new SecrecyAppClient(opts.uaJwt, this);
56
- this.db = new SecrecyDbClient(this);
57
57
  this.organization = new SecrecyOrganizationClient(this);
58
58
  this.wallet = new SecrecyWalletClient(this);
59
59
  this.pay = new SecrecyPayClient(this);
@@ -104,9 +104,7 @@ export class SecrecyClient extends BaseClient {
104
104
  });
105
105
  }
106
106
  async logout(sessionId) {
107
- nodesCache.clear();
108
- dataCache.clear();
109
- publicKeysCache.clear();
107
+ clearAllCaches();
110
108
  await super.logout(sessionId);
111
109
  }
112
110
  }
@@ -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 { derivePassword, generatePassword } from '../crypto/helpers.js';
12
- import { decryptCryptoBox, encryptSecretBox } from '../crypto/index.js';
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
- let { encryptedData, encryptedDataKey, dataKey, md5Data, md5Encrypted } = encrypted
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(encryptSecretBox(key, derivedPassword)),
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: md5Data,
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: md5Data,
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
@@ -24,26 +24,22 @@ export const createTRPCClient = (opts) => innerCreateTRPCClient({
24
24
  : 'https://api.secrecy.tech/trpc',
25
25
  maxURLLength: 2083,
26
26
  fetch: async (input, init) => {
27
- const controller = new AbortController();
28
- const id = setTimeout(() => {
29
- controller.abort();
30
- }, 20000);
27
+ const timeout = AbortSignal.timeout(opts.timeoutMs ?? 20_000);
28
+ const signal = init?.signal
29
+ ? AbortSignal.any([init.signal, timeout])
30
+ : timeout;
31
31
  const headers = new Headers(init?.headers);
32
32
  headers.set('secrecy-lib-version', SECRECY_LIB_VERSION);
33
33
  if (typeof opts.session === 'string') {
34
34
  headers.set('secrecy-session', opts.session);
35
35
  }
36
- const call = fetch(input, {
36
+ return await fetch(input, {
37
37
  ...init,
38
38
  headers,
39
- signal: controller.signal,
39
+ signal,
40
40
  mode: 'cors',
41
41
  credentials: opts.session ? 'include' : 'omit',
42
42
  });
43
- void call.finally(() => {
44
- clearTimeout(id);
45
- });
46
- return await call;
47
43
  },
48
44
  }),
49
45
  ],