@@ -24,15 +24,21 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
24
24
import { isSecureBackupRequired } from './utils/WellKnownUtils' ;
25
25
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog' ;
26
26
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog' ;
27
+ import SettingsStore from "./settings/SettingsStore" ;
27
28
28
29
// This stores the secret storage private keys in memory for the JS SDK. This is
29
30
// only meant to act as a cache to avoid prompting the user multiple times
30
31
// during the same single operation. Use `accessSecretStorage` below to scope a
31
32
// single secret storage operation, as it will clear the cached keys once the
32
33
// operation ends.
33
34
let secretStorageKeys = { } ;
35
+ let secretStorageKeyInfo = { } ;
34
36
let secretStorageBeingAccessed = false ;
35
37
38
+ let nonInteractive = false ;
39
+
40
+ let dehydrationCache = { } ;
41
+
36
42
function isCachingAllowed ( ) {
37
43
return secretStorageBeingAccessed ;
38
44
}
@@ -66,6 +72,20 @@ async function confirmToDismiss() {
66
72
return ! sure ;
67
73
}
68
74
75
+ function makeInputToKey ( keyInfo ) {
76
+ return async ( { passphrase, recoveryKey } ) => {
77
+ if ( passphrase ) {
78
+ return deriveKey (
79
+ passphrase ,
80
+ keyInfo . passphrase . salt ,
81
+ keyInfo . passphrase . iterations ,
82
+ ) ;
83
+ } else {
84
+ return decodeRecoveryKey ( recoveryKey ) ;
85
+ }
86
+ } ;
87
+ }
88
+
69
89
async function getSecretStorageKey ( { keys : keyInfos } , ssssItemName ) {
70
90
const keyInfoEntries = Object . entries ( keyInfos ) ;
71
91
if ( keyInfoEntries . length > 1 ) {
@@ -78,17 +98,18 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
78
98
return [ keyId , secretStorageKeys [ keyId ] ] ;
79
99
}
80
100
81
- const inputToKey = async ( { passphrase, recoveryKey } ) => {
82
- if ( passphrase ) {
83
- return deriveKey (
84
- passphrase ,
85
- keyInfo . passphrase . salt ,
86
- keyInfo . passphrase . iterations ,
87
- ) ;
88
- } else {
89
- return decodeRecoveryKey ( recoveryKey ) ;
101
+ if ( dehydrationCache . key ) {
102
+ if ( await MatrixClientPeg . get ( ) . checkSecretStorageKey ( dehydrationCache . key , keyInfo ) ) {
103
+ cacheSecretStorageKey ( keyId , dehydrationCache . key , keyInfo ) ;
104
+ return [ keyId , dehydrationCache . key ] ;
90
105
}
91
- } ;
106
+ }
107
+
108
+ if ( nonInteractive ) {
109
+ throw new Error ( "Could not unlock non-interactively" ) ;
110
+ }
111
+
112
+ const inputToKey = makeInputToKey ( keyInfo ) ;
92
113
const { finished } = Modal . createTrackedDialog ( "Access Secret Storage dialog" , "" ,
93
114
AccessSecretStorageDialog ,
94
115
/* props= */
@@ -118,14 +139,56 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
118
139
const key = await inputToKey ( input ) ;
119
140
120
141
// Save to cache to avoid future prompts in the current session
121
- cacheSecretStorageKey ( keyId , key ) ;
142
+ cacheSecretStorageKey ( keyId , key , keyInfo ) ;
122
143
123
144
return [ keyId , key ] ;
124
145
}
125
146
126
- function cacheSecretStorageKey ( keyId , key ) {
147
+ export async function getDehydrationKey ( keyInfo , checkFunc ) {
148
+ const inputToKey = makeInputToKey ( keyInfo ) ;
149
+ const { finished } = Modal . createTrackedDialog ( "Access Secret Storage dialog" , "" ,
150
+ AccessSecretStorageDialog ,
151
+ /* props= */
152
+ {
153
+ keyInfo,
154
+ checkPrivateKey : async ( input ) => {
155
+ const key = await inputToKey ( input ) ;
156
+ try {
157
+ checkFunc ( key ) ;
158
+ return true ;
159
+ } catch ( e ) {
160
+ return false ;
161
+ }
162
+ } ,
163
+ } ,
164
+ /* className= */ null ,
165
+ /* isPriorityModal= */ false ,
166
+ /* isStaticModal= */ false ,
167
+ /* options= */ {
168
+ onBeforeClose : async ( reason ) => {
169
+ if ( reason === "backgroundClick" ) {
170
+ return confirmToDismiss ( ) ;
171
+ }
172
+ return true ;
173
+ } ,
174
+ } ,
175
+ ) ;
176
+ const [ input ] = await finished ;
177
+ if ( ! input ) {
178
+ throw new AccessCancelledError ( ) ;
179
+ }
180
+ const key = await inputToKey ( input ) ;
181
+
182
+ // need to copy the key because rehydration (unpickling) will clobber it
183
+ dehydrationCache = { key : new Uint8Array ( key ) , keyInfo} ;
184
+
185
+ return key ;
186
+ }
187
+
188
+ function cacheSecretStorageKey ( keyId , key , keyInfo ) {
127
189
if ( isCachingAllowed ( ) ) {
128
190
secretStorageKeys [ keyId ] = key ;
191
+ secretStorageKeyInfo [ keyId ] = keyInfo ;
129
192
}
130
193
}
131
194
@@ -176,6 +239,7 @@ export const crossSigningCallbacks = {
176
239
getSecretStorageKey,
177
240
cacheSecretStorageKey,
178
241
onSecretRequested,
242
+ getDehydrationKey,
179
243
} ;
180
244
181
245
export async function promptForBackupPassphrase ( ) {
@@ -262,6 +326,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
262
326
await cli . bootstrapSecretStorage ( {
263
327
getKeyBackupPassphrase : promptForBackupPassphrase ,
264
328
} ) ;
329
+
330
+ const keyId = Object . keys ( secretStorageKeys ) [ 0 ] ;
331
+ if ( keyId && SettingsStore . getValue ( "feature_dehydration" ) ) {
332
+ const dehydrationKeyInfo =
333
+ secretStorageKeyInfo [ keyId ] && secretStorageKeyInfo [ keyId ] . passphrase
334
+ ? { passphrase : secretStorageKeyInfo [ keyId ] . passphrase }
335
+ : { } ;
336
+ console . log ( "Setting dehydration key" ) ;
337
+ await cli . setDehydrationKey ( secretStorageKeys [ keyId ] , dehydrationKeyInfo , "Backup device" ) ;
338
+ } else {
339
+ console . log ( "Not setting dehydration key: no SSSS key found" ) ;
340
+ }
265
341
}
266
342
267
343
// `return await` needed here to ensure `finally` block runs after the
@@ -272,6 +348,57 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
272
348
secretStorageBeingAccessed = false ;
273
349
if ( ! isCachingAllowed ( ) ) {
274
350
secretStorageKeys = { } ;
351
+ secretStorageKeyInfo = { } ;
352
+ }
353
+ }
354
+ }
355
+
356
+ // FIXME: this function name is a bit of a mouthful
357
+ export async function tryToUnlockSecretStorageWithDehydrationKey ( client ) {
358
+ const key = dehydrationCache . key ;
359
+ let restoringBackup = false ;
360
+ if ( key && await client . isSecretStorageReady ( ) ) {
361
+ console . log ( "Trying to set up cross-signing using dehydration key" ) ;
362
+ secretStorageBeingAccessed = true ;
363
+ nonInteractive = true ;
364
+ try {
365
+ await client . checkOwnCrossSigningTrust ( ) ;
366
+
367
+ // we also need to set a new dehydrated device to replace the
368
+ // device we rehydrated
369
+ const dehydrationKeyInfo =
370
+ dehydrationCache . keyInfo && dehydrationCache . keyInfo . passphrase
371
+ ? { passphrase : dehydrationCache . keyInfo . passphrase }
372
+ : { } ;
373
+ await client . setDehydrationKey ( key , dehydrationKeyInfo , "Backup device" ) ;
374
+
375
+ // and restore from backup
376
+ const backupInfo = await client . getKeyBackupVersion ( ) ;
377
+ if ( backupInfo ) {
378
+ restoringBackup = true ;
379
+ // don't await, because this can take a long time
380
+ client . restoreKeyBackupWithSecretStorage ( backupInfo )
381
+ . finally ( ( ) => {
382
+ secretStorageBeingAccessed = false ;
383
+ nonInteractive = false ;
384
+ if ( ! isCachingAllowed ( ) ) {
385
+ secretStorageKeys = { } ;
386
+ secretStorageKeyInfo = { } ;
387
+ }
388
+ } ) ;
389
+ }
390
+ } finally {
391
+ dehydrationCache = { } ;
392
+ // the secret storage cache is needed for restoring from backup, so
393
+ // don't clear it yet if we're restoring from backup
394
+ if ( ! restoringBackup ) {
395
+ secretStorageBeingAccessed = false ;
396
+ nonInteractive = false ;
397
+ if ( ! isCachingAllowed ( ) ) {
398
+ secretStorageKeys = { } ;
399
+ secretStorageKeyInfo = { } ;
400
+ }
401
+ }
275
402
}
276
403
}
277
404
}
0 commit comments