Skip to content

Commit 68529c4

Browse files
committed
feat: Simplified bootstrap with crypto identity extension
1 parent da1e7c6 commit 68529c4

File tree

7 files changed

+316
-8
lines changed

7 files changed

+316
-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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
Future<({bool initialized, bool connected})> getCryptoIdentityState() async =>
9+
(
10+
initialized: (encryption?.keyManager.enabled ?? false) &&
11+
(encryption?.crossSigning.enabled ?? false),
12+
connected: ((await encryption?.keyManager.isCached()) ?? false) &&
13+
((await encryption?.crossSigning.isCached()) ?? false),
14+
);
15+
16+
Future<void> restoreCryptoIdentity(String keyOrPassphrase) async {
17+
final encryption = this.encryption;
18+
if (encryption == null) {
19+
throw Exception('End to end encryption not available!');
20+
}
21+
final cryptoIdentityState = await getCryptoIdentityState();
22+
if (!cryptoIdentityState.initialized) {
23+
throw Exception(
24+
'Crypto identity is not initalized. Please check with `Client.getCryptoIdentityState()` first and run `Client.initCryptoIdentity()` once for this account.',
25+
);
26+
}
27+
if (cryptoIdentityState.connected) {
28+
throw Exception(
29+
'Crypto identity is already connected. Please check with `Client.getCryptoIdentityState()`.',
30+
);
31+
}
32+
33+
final completer = Completer();
34+
encryption.bootstrap(
35+
onUpdate: (bootstrap) async {
36+
try {
37+
switch (bootstrap.state) {
38+
case BootstrapState.loading:
39+
break;
40+
case BootstrapState.askWipeSsss:
41+
bootstrap.wipeSsss(false);
42+
break;
43+
case BootstrapState.askUseExistingSsss:
44+
bootstrap.useExistingSsss(true);
45+
break;
46+
case BootstrapState.askUnlockSsss:
47+
bootstrap.unlockedSsss();
48+
break;
49+
case BootstrapState.askBadSsss:
50+
bootstrap.ignoreBadSecrets(false);
51+
break;
52+
case BootstrapState.openExistingSsss:
53+
await bootstrap.newSsssKey!
54+
.unlock(keyOrPassphrase: keyOrPassphrase);
55+
await bootstrap.openExistingSsss();
56+
await bootstrap.client.encryption!.crossSigning
57+
.selfSign(recoveryKey: keyOrPassphrase);
58+
break;
59+
case BootstrapState.askWipeCrossSigning:
60+
await bootstrap.wipeCrossSigning(false);
61+
break;
62+
case BootstrapState.askWipeOnlineKeyBackup:
63+
bootstrap.wipeOnlineKeyBackup(false);
64+
break;
65+
// These states should not appear at all:
66+
case BootstrapState.askSetupOnlineKeyBackup:
67+
case BootstrapState.askSetupCrossSigning:
68+
case BootstrapState.askNewSsss:
69+
throw Exception(
70+
'Bootstrap state ${bootstrap.state} should not happen!',
71+
);
72+
case BootstrapState.error:
73+
throw Exception('Bootstrap error!');
74+
case BootstrapState.done:
75+
completer.complete();
76+
break;
77+
}
78+
} catch (e, s) {
79+
if (completer.isCompleted) {
80+
return Logs().e('Bootstrap error after completed', e, s);
81+
}
82+
return completer.completeError(e, s);
83+
}
84+
},
85+
);
86+
87+
await completer.future;
88+
}
89+
90+
Future<String> initCryptoIdentity({
91+
String? passphrase,
92+
bool wipeSecureStorage = true,
93+
bool wipeKeyBackup = true,
94+
bool wipeCrossSigning = true,
95+
bool setupMasterKey = true,
96+
bool setupSelfSigningKey = true,
97+
bool setupUserSigningKey = true,
98+
bool setupOnlineKeyBackup = true,
99+
}) async {
100+
final encryption = this.encryption;
101+
if (encryption == null) {
102+
throw Exception('End to end encryption not available!');
103+
}
104+
105+
String? newSsssKey;
106+
final completer = Completer();
107+
encryption.bootstrap(
108+
onUpdate: (bootstrap) async {
109+
try {
110+
newSsssKey ??= bootstrap.newSsssKey?.recoveryKey;
111+
switch (bootstrap.state) {
112+
case BootstrapState.loading:
113+
break;
114+
case BootstrapState.askWipeSsss:
115+
bootstrap.wipeSsss(wipeSecureStorage);
116+
break;
117+
case BootstrapState.askUseExistingSsss:
118+
bootstrap.useExistingSsss(false);
119+
break;
120+
case BootstrapState.askUnlockSsss:
121+
bootstrap.unlockedSsss();
122+
break;
123+
case BootstrapState.askBadSsss:
124+
bootstrap.ignoreBadSecrets(true);
125+
break;
126+
case BootstrapState.askWipeCrossSigning:
127+
await bootstrap.wipeCrossSigning(wipeCrossSigning);
128+
break;
129+
case BootstrapState.askWipeOnlineKeyBackup:
130+
bootstrap.wipeOnlineKeyBackup(wipeKeyBackup);
131+
break;
132+
case BootstrapState.askSetupOnlineKeyBackup:
133+
await bootstrap.askSetupOnlineKeyBackup(setupOnlineKeyBackup);
134+
break;
135+
case BootstrapState.askSetupCrossSigning:
136+
await bootstrap.askSetupCrossSigning(
137+
setupMasterKey: setupMasterKey,
138+
setupSelfSigningKey: setupSelfSigningKey,
139+
setupUserSigningKey: setupUserSigningKey,
140+
);
141+
break;
142+
case BootstrapState.askNewSsss:
143+
await bootstrap.newSsss(passphrase);
144+
break;
145+
case BootstrapState.openExistingSsss:
146+
throw Exception(
147+
'Bootstrap state ${bootstrap.state} should not happen!',
148+
);
149+
case BootstrapState.error:
150+
throw Exception('Bootstrap error!');
151+
case BootstrapState.done:
152+
completer.complete();
153+
break;
154+
}
155+
} catch (e, s) {
156+
if (completer.isCompleted) {
157+
return Logs().e('Bootstrap error after completed', e, s);
158+
}
159+
return completer.completeError(e, s);
160+
}
161+
},
162+
);
163+
164+
await completer.future;
165+
return newSsssKey!;
166+
}
167+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Famedly Matrix SDK
3+
* Copyright (C) 2020 Famedly GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:test/test.dart';
20+
import 'package:vodozemac/vodozemac.dart' as vod;
21+
22+
import 'package:matrix/encryption/utils/crypto_setup_extension.dart';
23+
import 'package:matrix/matrix.dart';
24+
import '../fake_client.dart';
25+
26+
void main() {
27+
group('Bootstrap', tags: 'olm', () {
28+
Logs().level = Level.error;
29+
30+
late Client client;
31+
32+
setUpAll(() async {
33+
await vod.init(
34+
wasmPath: './pkg/',
35+
libraryPath: './rust/target/debug/',
36+
);
37+
38+
client = await getClient();
39+
});
40+
41+
test('getCryptoIdentityState', () async {
42+
final state = await client.getCryptoIdentityState();
43+
expect(state.initialized, true);
44+
expect(state.connected, false);
45+
});
46+
47+
test('initCryptoIdentity & restoreCryptoIdentity', () async {
48+
var state = await client.getCryptoIdentityState();
49+
expect(state.initialized, true);
50+
expect(state.connected, false);
51+
52+
final recoveryKey = await client.initCryptoIdentity();
53+
expect(recoveryKey.length, 59);
54+
expect(recoveryKey.substring(0, 2), 'Es');
55+
56+
state = await client.getCryptoIdentityState();
57+
expect(state.initialized, true);
58+
expect(state.connected, true);
59+
60+
await client.encryption!.ssss.clearCache();
61+
62+
state = await client.getCryptoIdentityState();
63+
expect(state.initialized, true);
64+
expect(state.connected, false);
65+
66+
await client.restoreCryptoIdentity(recoveryKey);
67+
68+
state = await client.getCryptoIdentityState();
69+
expect(state.initialized, true);
70+
expect(state.connected, true);
71+
});
72+
});
73+
}

test_driver/dendrite/data/dendrite.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ client_api:
185185
# will be released after the cooloff time in milliseconds. Server administrators
186186
# and appservice users are exempt from rate limiting by default.
187187
rate_limiting:
188-
enabled: true
188+
enabled: false
189189
threshold: 5
190190
cooloff_ms: 500
191191
exempt_user_ids:

test_driver/matrixsdk_test.dart

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'dart:io';
2121
import 'package:test/test.dart';
2222
import 'package:vodozemac/vodozemac.dart' as vod;
2323

24+
import 'package:matrix/encryption/utils/crypto_setup_extension.dart';
2425
import 'package:matrix/matrix.dart';
2526
import '../test/fake_database.dart';
2627
import 'test_config.dart';
@@ -458,8 +459,31 @@ void main() => group(
458459
"++++ (Bob) Received decrypted message: '${inviteRoom.lastEvent!.body}' ++++",
459460
);
460461

461-
await room.leave();
462-
await room.forget();
462+
Logs().i('++++ (Alice) Init crypto identity ++++');
463+
if (Platform.environment['HOMESERVER_IMPLEMENTATION'] !=
464+
'conduit') {
465+
const passphrase = 'aliceSecurePassphrase100%';
466+
await testClientA.initCryptoIdentity(passphrase: passphrase);
467+
await testClientA.logout();
468+
await testClientA.checkHomeserver(homeserverUri);
469+
await testClientA.login(
470+
LoginType.mLoginPassword,
471+
identifier:
472+
AuthenticationUserIdentifier(user: Users.user1.name),
473+
password: Users.user1.password,
474+
);
475+
await testClientA.oneShotSync();
476+
await testClientA.restoreCryptoIdentity(passphrase);
477+
final newSessionRoomA = testClientA.getRoomById(roomId)!;
478+
await newSessionRoomA.lastEvent?.requestKey();
479+
expect(newSessionRoomA.lastEvent!.body, testMessage6);
480+
await newSessionRoomA.leave();
481+
await newSessionRoomA.forget();
482+
} else {
483+
await room.leave();
484+
await room.forget();
485+
}
486+
463487
await inviteRoom.leave();
464488
await inviteRoom.forget();
465489
await Future.delayed(Duration(seconds: 1));

test_driver/synapse/data/homeserver.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -865,10 +865,10 @@ log_config: "/data/localhost.log.config"
865865
# per_second: 0.1
866866
# burst_count: 5
867867
#
868-
#rc_login:
869-
# address:
870-
# per_second: 0.17
871-
# burst_count: 3
868+
rc_login:
869+
address:
870+
per_second: 1
871+
burst_count: 100
872872
# account:
873873
# per_second: 0.17
874874
# burst_count: 3

0 commit comments

Comments
 (0)