Skip to content

Commit 1ebc7cd

Browse files
committed
feat: Simplified bootstrap with crypto identity extension
1 parent 7fd01a9 commit 1ebc7cd

File tree

7 files changed

+430
-8
lines changed

7 files changed

+430
-8
lines changed

doc/end-to-end-encryption.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,47 @@ final client = Client('Matrix Client',
3737
),
3838
// ...
3939
);
40-
```
40+
```
41+
42+
### Setup your crypto identity
43+
44+
To use **Secure Storage and Sharing**, **Cross Signing** and the **Online Key Backup**,
45+
you should set up your crypto identity. The crypto identity is defined as the
46+
combined feature of those three features. First you should check if it is already
47+
set up for this account:
48+
49+
```dart
50+
final state = await client.getCryptoIdentityState();
51+
if (state.initialized) {
52+
print('Your crypto identity is initialized. You can either restore or wipe it.');
53+
}
54+
if (state.connected) {
55+
print('Your crypto identity is initialized and you are connected. You can now only wipe it to reset your passphrase or recovery key!');
56+
}
57+
```
58+
59+
If `initialized` is `false` you need to initialize your crypto identity first:
60+
61+
```dart
62+
final recoveryKey = await client.initCryptoIdentity();
63+
```
64+
65+
You can also set a custom passphrase:
66+
67+
```dart
68+
final passphrase = await client.initCryptoIdentity('SuperSecurePassphrase154%');
69+
```
70+
71+
To then reconnect on a new device you can restore your crypto identity:
72+
73+
```dart
74+
await client.restoreCryptoIdentity(passphraseOrRecoveryKey);
75+
```
76+
77+
If you have lost your passphrase or recovery key, you can wipe your crypto
78+
identity and get a new key with `client.initCryptoIdentity()` at any time.
79+
80+
> hint: An alternative to `client.restoreCryptoIdentity()` can be that you use
81+
> **key verification** to connect with another session which is already connected.
82+
> The Client would then request all necessary secrets of your crypto identity
83+
> automatically via **to-device-messaging**.

lib/encryption.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export 'encryption/key_manager.dart';
2424
export 'encryption/ssss.dart';
2525
export 'encryption/utils/key_verification.dart';
2626
export 'encryption/utils/bootstrap.dart';
27+
export 'encryption/utils/crypto_setup_extension.dart';
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import 'dart:async';
2+
3+
import 'package:matrix/encryption/utils/bootstrap.dart';
4+
import 'package:matrix/matrix.dart';
5+
6+
extension CryptoSetupExtension on Client {
7+
/// Returns the current state of the crypto identity.
8+
/// The crypto identity is `initialized` if key backup and cross signing
9+
/// are correctly set up. You can initialize a new account by using
10+
/// `Client.initCryptoIdentity()`.
11+
/// The crypto identity is `connected` if this device has all the secrets
12+
/// cached locally. This usually includes that this device has signed itself.
13+
/// You can use `Client.restoreCryptoIdentity()` to connect or
14+
/// `Client.initCryptoIdentity()` to wipe the current identity in case of
15+
/// that you lost your recovery key / passphrase and have no other way
16+
/// to restore.
17+
Future<({bool initialized, bool connected})> getCryptoIdentityState() async =>
18+
(
19+
initialized: (encryption?.keyManager.enabled ?? false) &&
20+
(encryption?.crossSigning.enabled ?? false),
21+
connected: ((await encryption?.keyManager.isCached()) ?? false) &&
22+
((await encryption?.crossSigning.isCached()) ?? false),
23+
);
24+
25+
/// Reconnects to an already initialized crypto identity using the provided
26+
/// recovery key or passphrase. Throws if encryption is unavailable, the
27+
/// identity is not initialized, or it is already connected.
28+
///
29+
/// [keyOrPassphrase] is the recovery key or passphrase that unlocks the
30+
/// secure secret storage. [keyIdentifier] can select a specific key when
31+
/// multiple exist.
32+
Future<void> restoreCryptoIdentity(
33+
String keyOrPassphrase, {
34+
String? keyIdentifier,
35+
bool selfSign = true,
36+
}) async {
37+
final encryption = this.encryption;
38+
if (encryption == null) {
39+
throw Exception('End to end encryption not available!');
40+
}
41+
final cryptoIdentityState = await getCryptoIdentityState();
42+
if (!cryptoIdentityState.initialized) {
43+
throw Exception(
44+
'Crypto identity is not initalized. Please check with `Client.getCryptoIdentityState()` first and run `Client.initCryptoIdentity()` once for this account.',
45+
);
46+
}
47+
if (cryptoIdentityState.connected) {
48+
throw Exception(
49+
'Crypto identity is already connected. Please check with `Client.getCryptoIdentityState()`.',
50+
);
51+
}
52+
53+
final completer = Completer();
54+
encryption.bootstrap(
55+
onUpdate: (bootstrap) async {
56+
try {
57+
switch (bootstrap.state) {
58+
case BootstrapState.loading:
59+
break;
60+
case BootstrapState.askWipeSsss:
61+
bootstrap.wipeSsss(false);
62+
break;
63+
case BootstrapState.askUseExistingSsss:
64+
bootstrap.useExistingSsss(true, keyIdentifier: keyIdentifier);
65+
break;
66+
case BootstrapState.askUnlockSsss:
67+
bootstrap.unlockedSsss();
68+
break;
69+
case BootstrapState.askBadSsss:
70+
bootstrap.ignoreBadSecrets(false);
71+
break;
72+
case BootstrapState.openExistingSsss:
73+
await bootstrap.newSsssKey!
74+
.unlock(keyOrPassphrase: keyOrPassphrase);
75+
await bootstrap.openExistingSsss();
76+
if (selfSign) {
77+
await bootstrap.client.encryption!.crossSigning
78+
.selfSign(keyOrPassphrase: keyOrPassphrase);
79+
}
80+
break;
81+
case BootstrapState.askWipeCrossSigning:
82+
await bootstrap.wipeCrossSigning(false);
83+
break;
84+
case BootstrapState.askWipeOnlineKeyBackup:
85+
bootstrap.wipeOnlineKeyBackup(false);
86+
break;
87+
// These states should not appear at all:
88+
case BootstrapState.askSetupOnlineKeyBackup:
89+
case BootstrapState.askSetupCrossSigning:
90+
case BootstrapState.askNewSsss:
91+
throw Exception(
92+
'Bootstrap state ${bootstrap.state} should not happen!',
93+
);
94+
case BootstrapState.error:
95+
throw Exception('Bootstrap error!');
96+
case BootstrapState.done:
97+
completer.complete();
98+
break;
99+
}
100+
} catch (e, s) {
101+
if (completer.isCompleted) {
102+
return Logs().e('Bootstrap error after completed', e, s);
103+
}
104+
return completer.completeError(e, s);
105+
}
106+
},
107+
);
108+
109+
await completer.future;
110+
}
111+
112+
/// Bootsraps a new crypto identity for the client. Creates secret storage
113+
/// and cross-signing keys and optionally online key backup. Returns the
114+
/// generated recovery key when secret storage is newly created.
115+
///
116+
/// [passphrase] lets users remember a human-readable phrase from which the
117+
/// recovery key is derived using PBKDF2.
118+
/// When [wipeSecureStorage] or [wipeKeyBackup] or [wipeCrossSigning] are true,
119+
/// existing data is wiped during setup.
120+
/// The `setup*` flags control which cross-signing keys and key backup are
121+
/// provisioned. [keyName] can label the generated secret storage key.
122+
Future<String> initCryptoIdentity({
123+
String? passphrase,
124+
bool wipeSecureStorage = true,
125+
bool wipeKeyBackup = true,
126+
bool wipeCrossSigning = true,
127+
bool setupMasterKey = true,
128+
bool setupSelfSigningKey = true,
129+
bool setupUserSigningKey = true,
130+
bool setupOnlineKeyBackup = true,
131+
String? keyName,
132+
}) async {
133+
final encryption = this.encryption;
134+
if (encryption == null) {
135+
throw Exception('End to end encryption not available!');
136+
}
137+
138+
String? newSsssKey;
139+
final completer = Completer();
140+
encryption.bootstrap(
141+
onUpdate: (bootstrap) async {
142+
try {
143+
newSsssKey ??= bootstrap.newSsssKey?.recoveryKey;
144+
switch (bootstrap.state) {
145+
case BootstrapState.loading:
146+
break;
147+
case BootstrapState.askWipeSsss:
148+
bootstrap.wipeSsss(wipeSecureStorage);
149+
break;
150+
case BootstrapState.askUseExistingSsss:
151+
bootstrap.useExistingSsss(false);
152+
break;
153+
case BootstrapState.askUnlockSsss:
154+
bootstrap.unlockedSsss();
155+
break;
156+
case BootstrapState.askBadSsss:
157+
bootstrap.ignoreBadSecrets(true);
158+
break;
159+
case BootstrapState.askWipeCrossSigning:
160+
await bootstrap.wipeCrossSigning(wipeCrossSigning);
161+
break;
162+
case BootstrapState.askWipeOnlineKeyBackup:
163+
bootstrap.wipeOnlineKeyBackup(wipeKeyBackup);
164+
break;
165+
case BootstrapState.askSetupOnlineKeyBackup:
166+
await bootstrap.askSetupOnlineKeyBackup(setupOnlineKeyBackup);
167+
break;
168+
case BootstrapState.askSetupCrossSigning:
169+
await bootstrap.askSetupCrossSigning(
170+
setupMasterKey: setupMasterKey,
171+
setupSelfSigningKey: setupSelfSigningKey,
172+
setupUserSigningKey: setupUserSigningKey,
173+
);
174+
break;
175+
case BootstrapState.askNewSsss:
176+
await bootstrap.newSsss(passphrase, keyName);
177+
break;
178+
case BootstrapState.openExistingSsss:
179+
throw Exception(
180+
'Bootstrap state ${bootstrap.state} should not happen!',
181+
);
182+
case BootstrapState.error:
183+
throw Exception('Bootstrap error!');
184+
case BootstrapState.done:
185+
completer.complete();
186+
break;
187+
}
188+
} catch (e, s) {
189+
if (completer.isCompleted) {
190+
return Logs().e('Bootstrap error after completed', e, s);
191+
}
192+
return completer.completeError(e, s);
193+
}
194+
},
195+
);
196+
197+
await completer.future;
198+
return newSsssKey!;
199+
}
200+
}

0 commit comments

Comments
 (0)