@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.
- package/dist/lib/base-client.js +4 -27
- package/dist/lib/client/SecrecyAppClient.js +17 -13
- package/dist/lib/client/SecrecyCloudClient.js +156 -368
- package/dist/lib/client/SecrecyDbClient.js +7 -3
- package/dist/lib/client/SecrecyMailClient.js +48 -38
- package/dist/lib/client/SecrecyOrganizationClient.js +12 -10
- package/dist/lib/client/SecrecyPayClient.js +5 -1
- package/dist/lib/client/SecrecyPseudonymClient.js +8 -4
- package/dist/lib/client/SecrecyUserClient.js +11 -11
- package/dist/lib/client/SecrecyWalletClient.js +2 -0
- package/dist/lib/client/convert/data.js +4 -4
- package/dist/lib/client/convert/mail.js +6 -5
- package/dist/lib/client/convert/node.js +34 -46
- package/dist/lib/client/data-link.js +77 -0
- package/dist/lib/client/download.js +84 -0
- package/dist/lib/client/helpers.js +11 -18
- package/dist/lib/client/index.js +12 -48
- package/dist/lib/client/storage.js +2 -3
- package/dist/lib/client/types/index.js +7 -3
- package/dist/lib/client/upload.js +252 -0
- package/dist/lib/client.js +7 -0
- package/dist/lib/crypto/data.js +3 -0
- package/dist/lib/crypto/domain.js +123 -12
- package/dist/lib/crypto/helpers.js +23 -0
- package/dist/lib/index.js +0 -1
- package/dist/types/base-client.d.ts +1 -2
- package/dist/types/client/SecrecyAppClient.d.ts +3 -2
- package/dist/types/client/SecrecyCloudClient.d.ts +28 -20
- package/dist/types/client/SecrecyDbClient.d.ts +3 -1
- package/dist/types/client/SecrecyMailClient.d.ts +3 -2
- package/dist/types/client/SecrecyOrganizationClient.d.ts +3 -2
- package/dist/types/client/SecrecyPayClient.d.ts +3 -1
- package/dist/types/client/SecrecyPseudonymClient.d.ts +3 -2
- package/dist/types/client/SecrecyUserClient.d.ts +3 -2
- package/dist/types/client/convert/data.d.ts +3 -3
- package/dist/types/client/convert/mail.d.ts +5 -3
- package/dist/types/client/convert/node.d.ts +5 -5
- package/dist/types/client/data-link.d.ts +37 -0
- package/dist/types/client/download.d.ts +2 -0
- package/dist/types/client/index.d.ts +3 -11
- package/dist/types/client/storage.d.ts +2 -3
- package/dist/types/client/types/index.d.ts +9 -13
- package/dist/types/client/types/mail.d.ts +1 -2
- package/dist/types/client/types/node.d.ts +9 -12
- package/dist/types/client/types/user.d.ts +0 -15
- package/dist/types/client/upload.d.ts +38 -0
- package/dist/types/client.d.ts +6174 -681
- package/dist/types/crypto/domain.d.ts +36 -8
- package/dist/types/crypto/helpers.d.ts +12 -0
- package/dist/types/crypto/index.d.ts +3 -3
- package/dist/types/index.d.ts +1 -2
- package/package.json +4 -2
- package/dist/lib/client/types/identity.js +0 -18
- 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.
|
|
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
|
-
|
|
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
|
|
38
|
-
const keyPairs = storage.keyPairs.load();
|
|
35
|
+
const uaKeys = storage.userAppKeys.load();
|
|
39
36
|
const uaJwt = storage.jwt.load();
|
|
40
|
-
if (uaSession &&
|
|
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
|
-
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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)) {
|
package/dist/lib/client/index.js
CHANGED
|
@@ -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
|
-
#
|
|
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.#
|
|
42
|
-
this
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.#
|
|
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
|
|
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 {
|
|
6
|
+
return { userAppKeys, userAppSession, jwt };
|
|
8
7
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/lib/client.js
CHANGED
|
@@ -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
|
+
})();
|
package/dist/lib/crypto/data.js
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
}
|