@telecrypt-io/storage 0.8.0 → 0.9.2
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/LICENSE +1 -1
- package/README.md +57 -3
- package/dist/TeleCryptIOStorage.d.ts +32 -3
- package/dist/TeleCryptIOStorage.js +387 -70
- package/dist/core/errors.d.ts +6 -2
- package/dist/core/errors.js +4 -1
- package/dist/core/operations.d.ts +2 -0
- package/dist/core/operations.js +178 -79
- package/dist/core/types.d.ts +1 -0
- package/dist/key-safe.d.ts +62 -0
- package/dist/key-safe.js +349 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +10 -0
- package/dist/sharing.d.ts +3 -0
- package/dist/sharing.js +18 -0
- package/package.json +2 -2
package/dist/key-safe.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { MatrixError, MatrixEvent } from "matrix-js-sdk";
|
|
2
|
+
import { Method } from "matrix-js-sdk/lib/http-api/method.js";
|
|
3
|
+
import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
|
|
4
|
+
import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
|
|
5
|
+
import { encodeUri } from "matrix-js-sdk/lib/utils.js";
|
|
6
|
+
import { StorageError } from "./core/errors.js";
|
|
7
|
+
/** The local saved-key acknowledgement and Matrix's existing encryption setup. */
|
|
8
|
+
export class DecryptionKeySafe {
|
|
9
|
+
client;
|
|
10
|
+
withKey;
|
|
11
|
+
options;
|
|
12
|
+
startSync;
|
|
13
|
+
memory = {};
|
|
14
|
+
tail = Promise.resolve();
|
|
15
|
+
secretRecordsRefreshed = false;
|
|
16
|
+
refreshedKeyDescriptorId = null;
|
|
17
|
+
constructor(client, withKey, options = {}, startSync) {
|
|
18
|
+
this.client = client;
|
|
19
|
+
this.withKey = withKey;
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.startSync = startSync;
|
|
22
|
+
}
|
|
23
|
+
active(signal) {
|
|
24
|
+
if (signal?.aborted)
|
|
25
|
+
throw new StorageError("operation cancelled");
|
|
26
|
+
}
|
|
27
|
+
crypto() {
|
|
28
|
+
const crypto = this.client.getCrypto();
|
|
29
|
+
if (!crypto)
|
|
30
|
+
throw new StorageError("encryption is not initialized");
|
|
31
|
+
return crypto;
|
|
32
|
+
}
|
|
33
|
+
async local(write) {
|
|
34
|
+
if (this.options.persistentCryptoStore === false) {
|
|
35
|
+
if (write)
|
|
36
|
+
this.memory = write;
|
|
37
|
+
return this.memory;
|
|
38
|
+
}
|
|
39
|
+
const prefix = this.options.cryptoDatabasePrefix ?? `telecrypt-io-storage::${this.client.getUserId()}::${this.client.getDeviceId()}`;
|
|
40
|
+
const database = await new Promise((resolve, reject) => {
|
|
41
|
+
const request = indexedDB.open(`${prefix}::key-safe`, 1);
|
|
42
|
+
request.onupgradeneeded = () => request.result.createObjectStore("state");
|
|
43
|
+
request.onsuccess = () => resolve(request.result);
|
|
44
|
+
request.onerror = () => reject(request.error);
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
return await new Promise((resolve, reject) => {
|
|
48
|
+
const transaction = database.transaction("state", write ? "readwrite" : "readonly");
|
|
49
|
+
const store = transaction.objectStore("state");
|
|
50
|
+
const request = write ? store.put(write, "setup") : store.get("setup");
|
|
51
|
+
transaction.oncomplete = () => resolve(write ?? request.result ?? {});
|
|
52
|
+
transaction.onerror = () => reject(transaction.error);
|
|
53
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("key safe persistence aborted"));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
database.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async save(value) {
|
|
61
|
+
const previous = await this.local();
|
|
62
|
+
await this.local(value);
|
|
63
|
+
try {
|
|
64
|
+
await this.options.onKeySafeStateChanged?.();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await this.local(previous);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
serial(operation) {
|
|
72
|
+
const result = this.tail.then(operation);
|
|
73
|
+
this.tail = result.catch(() => undefined);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
async refreshAccountData(type) {
|
|
77
|
+
const user = this.client.getUserId();
|
|
78
|
+
if (!user)
|
|
79
|
+
throw new StorageError("Matrix login is not initialized");
|
|
80
|
+
const path = encodeUri("/user/$userId/account_data/$type", { $userId: user, $type: type });
|
|
81
|
+
let content;
|
|
82
|
+
try {
|
|
83
|
+
content = await this.client.http.authedRequest(Method.Get, path, undefined, undefined, { prefix: ClientPrefix.V3 });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(error instanceof MatrixError) || error.errcode !== "M_NOT_FOUND")
|
|
87
|
+
throw error;
|
|
88
|
+
content = null;
|
|
89
|
+
}
|
|
90
|
+
// An absent account-data event is not itself an event to cache. In
|
|
91
|
+
// particular, MatrixEvent normalizes null content to {}, which would make
|
|
92
|
+
// SecretStorage treat an absent secret as malformed content.
|
|
93
|
+
if (content !== null) {
|
|
94
|
+
this.client.store.storeAccountDataEvents([new MatrixEvent({ type, sender: user, content })]);
|
|
95
|
+
}
|
|
96
|
+
return content;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* MatrixClient.getAccountDataFromServer reads its local store after initial
|
|
100
|
+
* sync. A resumed sync can complete before account-data events reach that
|
|
101
|
+
* store, so refresh the existing Safe records from the authenticated server
|
|
102
|
+
* API before native secret-storage code reads them. Seed those exact records
|
|
103
|
+
* through the public Store API used by Matrix sync itself.
|
|
104
|
+
*/
|
|
105
|
+
async refreshSecretStorageAccountData(force = false) {
|
|
106
|
+
const defaultKey = await this.refreshAccountData("m.secret_storage.default_key");
|
|
107
|
+
const keyId = typeof defaultKey?.key === "string" ? defaultKey.key : null;
|
|
108
|
+
if (keyId && (force || this.refreshedKeyDescriptorId !== keyId)) {
|
|
109
|
+
await this.refreshAccountData(`m.secret_storage.key.${keyId}`);
|
|
110
|
+
this.refreshedKeyDescriptorId = keyId;
|
|
111
|
+
}
|
|
112
|
+
if (force || !this.secretRecordsRefreshed) {
|
|
113
|
+
await Promise.all([
|
|
114
|
+
this.refreshAccountData("m.cross_signing.master"),
|
|
115
|
+
this.refreshAccountData("m.cross_signing.self_signing"),
|
|
116
|
+
this.refreshAccountData("m.cross_signing.user_signing"),
|
|
117
|
+
this.refreshAccountData("m.megolm_backup.v1"),
|
|
118
|
+
]);
|
|
119
|
+
this.secretRecordsRefreshed = true;
|
|
120
|
+
}
|
|
121
|
+
return keyId;
|
|
122
|
+
}
|
|
123
|
+
async topology(forceAccountDataRefresh = false) {
|
|
124
|
+
const [defaultKeyId, backup] = await Promise.all([
|
|
125
|
+
this.refreshSecretStorageAccountData(forceAccountDataRefresh), this.serverBackup(),
|
|
126
|
+
]);
|
|
127
|
+
const key = await this.client.secretStorage.getKey();
|
|
128
|
+
return { key, defaultKeyId, backup };
|
|
129
|
+
}
|
|
130
|
+
async serverBackup() {
|
|
131
|
+
// CryptoApi.getKeyBackupInfo caches absence and maps request failures to
|
|
132
|
+
// null. Neither can authorize creating a new server backup on retry.
|
|
133
|
+
try {
|
|
134
|
+
return await this.client.http.authedRequest(Method.Get, "/room_keys/version", undefined, undefined, { prefix: ClientPrefix.V3 });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error instanceof MatrixError && error.errcode === "M_NOT_FOUND")
|
|
138
|
+
return null;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async signingReady() {
|
|
143
|
+
const crypto = this.crypto();
|
|
144
|
+
const user = this.client.getUserId();
|
|
145
|
+
await crypto.userHasCrossSigningKeys(user, true);
|
|
146
|
+
const [account, device, ready] = await Promise.all([
|
|
147
|
+
crypto.getUserVerificationStatus(user),
|
|
148
|
+
crypto.getDeviceVerificationStatus(user, this.client.getDeviceId()),
|
|
149
|
+
crypto.isCrossSigningReady(),
|
|
150
|
+
]);
|
|
151
|
+
return ready && account.isVerified() && device?.signedByOwner === true;
|
|
152
|
+
}
|
|
153
|
+
getStatus(signal) {
|
|
154
|
+
const options = { signal };
|
|
155
|
+
return this.serial(async () => {
|
|
156
|
+
this.active(options.signal);
|
|
157
|
+
const local = await this.local();
|
|
158
|
+
if (local.recoveryKey)
|
|
159
|
+
return { state: "confirmation-required", recoveryKey: local.recoveryKey };
|
|
160
|
+
const { key, defaultKeyId, backup } = await this.topology();
|
|
161
|
+
if (!key && !defaultKeyId && !backup)
|
|
162
|
+
return { state: "setup-required" };
|
|
163
|
+
if (local.confirmed && key?.[0] === local.keyId && backup?.version === local.backupVersion && await this.signingReady()) {
|
|
164
|
+
await this.crypto().checkKeyBackupAndEnable();
|
|
165
|
+
if (await this.crypto().getActiveSessionBackupVersion() === backup?.version) {
|
|
166
|
+
this.active(options.signal);
|
|
167
|
+
return { state: "ready" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.active(options.signal);
|
|
171
|
+
return { state: "restore-required" };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async requireReady(options = {}) {
|
|
175
|
+
const status = await this.getStatus(options.signal);
|
|
176
|
+
if (status.state !== "ready")
|
|
177
|
+
throw new StorageError("Set up or unlock the Decryption Key Safe before using storage");
|
|
178
|
+
}
|
|
179
|
+
/** Re-publish the same native signed records after an interrupted upload. */
|
|
180
|
+
async publishSigning(signal) {
|
|
181
|
+
const crypto = this.crypto();
|
|
182
|
+
const user = this.client.getUserId();
|
|
183
|
+
const cached = await crypto.getUserCrossSigningKeys(user);
|
|
184
|
+
const server = await this.client.downloadKeysForUsers([user]);
|
|
185
|
+
if (!cached?.master_key || !cached.self_signing_key || !cached.user_signing_key) {
|
|
186
|
+
throw new StorageError("account signing setup is incomplete; unlock the existing Decryption Key Safe");
|
|
187
|
+
}
|
|
188
|
+
for (const [remote, local] of [
|
|
189
|
+
[server.master_keys?.[user], cached.master_key],
|
|
190
|
+
[server.self_signing_keys?.[user], cached.self_signing_key],
|
|
191
|
+
[server.user_signing_keys?.[user], cached.user_signing_key],
|
|
192
|
+
]) {
|
|
193
|
+
if (remote && JSON.stringify(remote.keys) !== JSON.stringify(local.keys)) {
|
|
194
|
+
throw new StorageError("account signing keys changed; unlock the existing Decryption Key Safe");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.active(signal);
|
|
198
|
+
await this.client.http.authedRequest(Method.Post, "/keys/device_signing/upload", undefined, cached, { prefix: ClientPrefix.V3 });
|
|
199
|
+
this.active(signal);
|
|
200
|
+
await crypto.crossSignDevice(this.client.getDeviceId());
|
|
201
|
+
await crypto.userHasCrossSigningKeys(user, true);
|
|
202
|
+
}
|
|
203
|
+
async checkSigningCanResume() {
|
|
204
|
+
const user = this.client.getUserId();
|
|
205
|
+
const [cached, server, status] = await Promise.all([
|
|
206
|
+
this.crypto().getUserCrossSigningKeys(user),
|
|
207
|
+
this.client.downloadKeysForUsers([user]),
|
|
208
|
+
this.crypto().getCrossSigningStatus(),
|
|
209
|
+
]);
|
|
210
|
+
// A new login may defer room sync until the Safe is restored. Ensure the
|
|
211
|
+
// Rust crypto store has the account's public signing keys before importing
|
|
212
|
+
// their private copies from secret storage; the HTTP download above alone
|
|
213
|
+
// does not populate that native identity cache.
|
|
214
|
+
await this.crypto().userHasCrossSigningKeys(user, true);
|
|
215
|
+
const remote = server.master_keys?.[user];
|
|
216
|
+
const hasPrivateKeys = Object.values(status.privateKeysCachedLocally).every(Boolean);
|
|
217
|
+
if (remote && !status.privateKeysInSecretStorage && !hasPrivateKeys) {
|
|
218
|
+
throw new StorageError("Use the existing account signing keys; automatic replacement is not allowed");
|
|
219
|
+
}
|
|
220
|
+
if (remote && hasPrivateKeys && cached?.master_key && JSON.stringify(remote.keys) !== JSON.stringify(cached.master_key.keys)) {
|
|
221
|
+
throw new StorageError("Account signing keys changed; this login must unlock the existing Decryption Key Safe");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async configure(encoded, signal) {
|
|
225
|
+
const crypto = this.crypto();
|
|
226
|
+
const privateKey = decodeRecoveryKey(encoded);
|
|
227
|
+
const { key, defaultKeyId, backup } = await this.topology(true);
|
|
228
|
+
if (!key && defaultKeyId)
|
|
229
|
+
throw new StorageError("The existing Decryption Key Safe is incomplete; it will not be replaced");
|
|
230
|
+
if (key && !(await this.client.secretStorage.checkKey(privateKey, key[1]))) {
|
|
231
|
+
throw new StorageError("This key does not unlock the existing Decryption Key Safe");
|
|
232
|
+
}
|
|
233
|
+
await this.withKey(privateKey, async () => {
|
|
234
|
+
await this.checkSigningCanResume();
|
|
235
|
+
this.active(signal);
|
|
236
|
+
// Establish the encrypted account-data store first. Native cross-signing
|
|
237
|
+
// then saves its private keys there BEFORE publishing public keys. This
|
|
238
|
+
// keeps a CLI crash between native requests recoverable with the pending
|
|
239
|
+
// saved key, even before its next IndexedDB snapshot reaches disk.
|
|
240
|
+
if (!key) {
|
|
241
|
+
await crypto.bootstrapSecretStorage({
|
|
242
|
+
createSecretStorageKey: async () => ({ privateKey, encodedPrivateKey: encoded, keyInfo: {} }),
|
|
243
|
+
setupNewKeyBackup: false,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
this.active(signal);
|
|
247
|
+
// Omitting authUploadDeviceSigningKeys sends the real initial upload.
|
|
248
|
+
await crypto.bootstrapCrossSigning({});
|
|
249
|
+
await this.publishSigning(signal);
|
|
250
|
+
this.active(signal);
|
|
251
|
+
await crypto.bootstrapSecretStorage({
|
|
252
|
+
createSecretStorageKey: async () => ({ privateKey, encodedPrivateKey: encoded, keyInfo: {} }),
|
|
253
|
+
setupNewKeyBackup: backup === null,
|
|
254
|
+
});
|
|
255
|
+
this.active(signal);
|
|
256
|
+
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
|
257
|
+
await crypto.checkKeyBackupAndEnable();
|
|
258
|
+
if (!(await this.signingReady()) || !(await crypto.getActiveSessionBackupVersion())) {
|
|
259
|
+
throw new StorageError("Decryption Key Safe setup has not finished; retry with the same saved key");
|
|
260
|
+
}
|
|
261
|
+
}, signal);
|
|
262
|
+
}
|
|
263
|
+
setup(signal) {
|
|
264
|
+
const options = { signal };
|
|
265
|
+
return this.serial(async () => {
|
|
266
|
+
this.active(options.signal);
|
|
267
|
+
let local = await this.local();
|
|
268
|
+
if (!local.recoveryKey) {
|
|
269
|
+
const { key, defaultKeyId, backup } = await this.topology(true);
|
|
270
|
+
if (key || defaultKeyId || backup)
|
|
271
|
+
throw new StorageError("A Decryption Key Safe already exists; enter its saved key to unlock it");
|
|
272
|
+
const generated = await this.crypto().createRecoveryKeyFromPassphrase();
|
|
273
|
+
if (!generated.encodedPrivateKey)
|
|
274
|
+
throw new StorageError("Could not create the Decryption Key Safe key");
|
|
275
|
+
local = { recoveryKey: generated.encodedPrivateKey };
|
|
276
|
+
await this.save(local);
|
|
277
|
+
}
|
|
278
|
+
this.active(options.signal);
|
|
279
|
+
await this.configure(local.recoveryKey, options.signal);
|
|
280
|
+
return { recoveryKey: local.recoveryKey };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
confirmSaved(signal) {
|
|
284
|
+
const options = { signal };
|
|
285
|
+
return this.serial(async () => {
|
|
286
|
+
this.active(options.signal);
|
|
287
|
+
const local = await this.local();
|
|
288
|
+
if (!local.recoveryKey)
|
|
289
|
+
throw new StorageError("Set up the Decryption Key Safe and save its key before confirming");
|
|
290
|
+
await this.configure(local.recoveryKey, options.signal);
|
|
291
|
+
const { key, defaultKeyId, backup } = await this.topology(true);
|
|
292
|
+
if (!key && defaultKeyId)
|
|
293
|
+
throw new StorageError("The existing Decryption Key Safe is incomplete; it will not be replaced");
|
|
294
|
+
if (!key || !backup?.version)
|
|
295
|
+
throw new StorageError("Decryption Key Safe setup is incomplete");
|
|
296
|
+
this.active(options.signal);
|
|
297
|
+
await this.save({ confirmed: true, keyId: key[0], backupVersion: backup.version });
|
|
298
|
+
return { state: "ready" };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
restore(recoveryKey, signal) {
|
|
302
|
+
const options = { signal };
|
|
303
|
+
return this.serial(async () => {
|
|
304
|
+
this.active(options.signal);
|
|
305
|
+
let privateKey;
|
|
306
|
+
try {
|
|
307
|
+
privateKey = decodeRecoveryKey(recoveryKey);
|
|
308
|
+
}
|
|
309
|
+
catch (cause) {
|
|
310
|
+
throw new StorageError("Invalid Decryption Key Safe key", { cause });
|
|
311
|
+
}
|
|
312
|
+
const { key, defaultKeyId, backup } = await this.topology(true);
|
|
313
|
+
if (!key && defaultKeyId)
|
|
314
|
+
throw new StorageError("The existing Decryption Key Safe is incomplete; it will not be replaced");
|
|
315
|
+
if (!key || !(await this.client.secretStorage.checkKey(privateKey, key[1]))) {
|
|
316
|
+
throw new StorageError("This key does not unlock the existing Decryption Key Safe");
|
|
317
|
+
}
|
|
318
|
+
// An interrupted first setup may have saved secret storage before it
|
|
319
|
+
// created the backup. Complete those missing steps using this same key.
|
|
320
|
+
if (!backup)
|
|
321
|
+
await this.configure(recoveryKey, options.signal);
|
|
322
|
+
return this.withKey(privateKey, async () => {
|
|
323
|
+
const status = await this.crypto().getCrossSigningStatus();
|
|
324
|
+
if (!status.privateKeysInSecretStorage && !Object.values(status.privateKeysCachedLocally).every(Boolean)) {
|
|
325
|
+
throw new StorageError("Account signing keys are missing from the Decryption Key Safe; they will not be replaced");
|
|
326
|
+
}
|
|
327
|
+
await this.checkSigningCanResume();
|
|
328
|
+
this.active(options.signal);
|
|
329
|
+
await this.crypto().bootstrapCrossSigning({});
|
|
330
|
+
await this.publishSigning(options.signal);
|
|
331
|
+
await this.crypto().loadSessionBackupPrivateKeyFromSecretStorage();
|
|
332
|
+
await this.crypto().checkKeyBackupAndEnable();
|
|
333
|
+
const result = await this.crypto().restoreKeyBackup();
|
|
334
|
+
// Restore room keys before starting history sync. That first sync also
|
|
335
|
+
// publishes this login's device keys so Matrix can complete its
|
|
336
|
+
// account-signing check.
|
|
337
|
+
await this.startSync?.(options.signal);
|
|
338
|
+
if (!(await this.signingReady()))
|
|
339
|
+
throw new StorageError("This login's account signing setup is incomplete");
|
|
340
|
+
this.active(options.signal);
|
|
341
|
+
const currentBackup = await this.serverBackup();
|
|
342
|
+
if (!currentBackup?.version)
|
|
343
|
+
throw new StorageError("Decryption Key Safe backup is unavailable");
|
|
344
|
+
await this.save({ confirmed: true, keyId: key[0], backupVersion: currentBackup.version });
|
|
345
|
+
return { imported: result.imported, total: result.total, state: "ready" };
|
|
346
|
+
}, options.signal);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
package/dist/media.d.ts
ADDED
package/dist/media.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Method } from "matrix-js-sdk/lib/http-api/method.js";
|
|
2
|
+
import { ClientPrefix } from "matrix-js-sdk/lib/http-api/prefix.js";
|
|
3
|
+
import { StorageError } from "./core/errors.js";
|
|
4
|
+
/** The policy endpoint authorizes deletion against the uploading user. */
|
|
5
|
+
export async function deleteOwnedMedia(client, mediaIds, signal) {
|
|
6
|
+
if (!client.http || typeof client.http.authedRequest !== "function") {
|
|
7
|
+
throw new StorageError("Matrix HTTP transport unavailable");
|
|
8
|
+
}
|
|
9
|
+
await client.http.authedRequest(Method.Post, "/io.telecrypt.storage/delete_media", undefined, { media_ids: [...mediaIds] }, { prefix: ClientPrefix.Unstable, rawResponseBody: true, abortSignal: signal });
|
|
10
|
+
}
|
package/dist/sharing.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StorageError } from "./core/errors.js";
|
|
2
|
+
/** Use the same account/device requirements for explicit and inherited sharing. */
|
|
3
|
+
export async function prepareReaderHistorySharing(client, userId) {
|
|
4
|
+
const crypto = client.getCrypto();
|
|
5
|
+
if (!crypto)
|
|
6
|
+
throw new StorageError("sharing requires initialized encryption");
|
|
7
|
+
const recipient = await client.downloadKeysForUsers([userId]);
|
|
8
|
+
const devices = Object.keys(recipient.device_keys?.[userId] ?? {});
|
|
9
|
+
if (!recipient.master_keys?.[userId] || !recipient.self_signing_keys?.[userId] || devices.length === 0) {
|
|
10
|
+
throw new StorageError("The reader must set up or unlock their Decryption Key Safe before sharing");
|
|
11
|
+
}
|
|
12
|
+
return async () => {
|
|
13
|
+
const statuses = await Promise.all(devices.map((id) => crypto.getDeviceVerificationStatus(userId, id)));
|
|
14
|
+
if (!statuses.some((status) => status?.signedByOwner)) {
|
|
15
|
+
throw new StorageError("The invitation was sent, but history keys could not be shared; the reader must unlock their Decryption Key Safe, then retry sharing");
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telecrypt-io/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"license": "BUSL-1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.19.0",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"matrix-encrypt-attachment": "1.0.3",
|
|
42
|
-
"matrix-js-sdk": "42.
|
|
42
|
+
"matrix-js-sdk": "42.4.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "24.13.3",
|