add key verification test

This commit is contained in:
Sorunome 2020-06-15 13:12:59 +02:00
parent e2c358f319
commit c4d09268a0
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
3 changed files with 425 additions and 59 deletions

View file

@ -99,16 +99,16 @@ class KeyVerificationManager {
if (_requests.containsKey(transactionId)) { if (_requests.containsKey(transactionId)) {
final req = _requests[transactionId]; final req = _requests[transactionId];
final otherDeviceId = event['content']['from_device'];
if (event['sender'] != client.userID) { if (event['sender'] != client.userID) {
await req.handlePayload(type, event['content'], event['event_id']); await req.handlePayload(type, event['content'], event['event_id']);
} else if (req.userId == client.userID && req.deviceId == null) { } else if (event['sender'] == client.userID &&
// okay, maybe another of our devices answered otherDeviceId != null &&
await req.handlePayload(type, event['content'], event['event_id']); otherDeviceId != client.deviceID) {
if (req.deviceId != client.deviceID) { // okay, another of our devices answered
req.otherDeviceAccepted(); req.otherDeviceAccepted();
req.dispose(); req.dispose();
_requests.remove(transactionId); _requests.remove(transactionId);
}
} }
} else if (event['sender'] != client.userID) { } else if (event['sender'] != client.userID) {
final room = client.getRoomById(update.roomID) ?? final room = client.getRoomById(update.roomID) ??

View file

@ -16,12 +16,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart'; import 'package:famedlysdk/encryption.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import '../fake_client.dart'; import '../fake_client.dart';
import '../fake_matrix_api.dart';
class MockSSSS extends SSSS {
MockSSSS(Encryption encryption) : super(encryption);
bool requestedSecrets = false;
@override
Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
requestedSecrets = true;
final handle = open();
handle.unlock(recoveryKey: SSSS_KEY);
await handle.maybeCacheAll();
}
}
EventUpdate getLastSentEvent(KeyVerification req) {
final entry = FakeMatrixApi.calledEndpoints.entries
.firstWhere((p) => p.key.contains('/send/'));
final type = entry.key.split('/')[6];
final content = json.decode(entry.value.first);
return EventUpdate(
content: {
'event_id': req.transactionId,
'type': type,
'content': content,
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'sender': req.client.userID,
},
eventType: type,
type: 'timeline',
roomID: req.room.id,
);
}
void main() { void main() {
/// All Tests related to the ChatTime /// All Tests related to the ChatTime
@ -38,70 +73,392 @@ void main() {
if (!olmEnabled) return; if (!olmEnabled) return;
Client client; // key @othertest:fakeServer.notExisting
Room room; const otherPickledOlmAccount =
var updateCounter = 0; 'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
KeyVerification keyVerification;
Client client1;
Client client2;
test('setupClient', () async { test('setupClient', () async {
client = await getClient(); client1 = await getClient();
room = Room(id: '!localpart:server.abc', client: client); client2 =
keyVerification = KeyVerification( Client('othertestclient', debug: true, httpClient: FakeMatrixApi());
encryption: client.encryption, client2.database = client1.database;
room: room, await client2.checkServer('https://fakeServer.notExisting');
userId: '@alice:example.com', client2.connect(
deviceId: 'ABCD', newToken: 'abc',
onUpdate: () => updateCounter++, newUserID: '@othertest:fakeServer.notExisting',
newHomeserver: client2.api.homeserver,
newDeviceName: 'Text Matrix Client',
newDeviceID: 'FOXDEVICE',
newOlmAccount: otherPickledOlmAccount,
); );
await Future.delayed(Duration(milliseconds: 10));
client1.verificationMethods = {
KeyVerificationMethod.emoji,
KeyVerificationMethod.numbers
};
client2.verificationMethods = {
KeyVerificationMethod.emoji,
KeyVerificationMethod.numbers
};
}); });
test('acceptSas', () async { test('Run emoji / number verification', () async {
await keyVerification.acceptSas(); // for a full run we test in-room verification in a cleartext room
}); // because then we can easily intercept the payloads and inject in the other client
test('acceptVerification', () async { FakeMatrixApi.calledEndpoints.clear();
await keyVerification.acceptVerification(); // make sure our master key is *not* verified to not triger SSSS for now
}); client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
test('cancel', () async { final req1 =
await keyVerification.cancel('m.cancelcode'); await client1.userDeviceKeys[client2.userID].startVerification();
expect(keyVerification.canceled, true); var evt = getLastSentEvent(req1);
expect(keyVerification.canceledCode, 'm.cancelcode'); expect(req1.state, KeyVerificationState.waitingAccept);
expect(keyVerification.canceledReason, null);
}); KeyVerification req2;
test('handlePayload', () async { final sub = client2.onKeyVerificationRequest.stream.listen((req) {
await keyVerification.handlePayload('m.key.verification.request', { req2 = req;
'from_device': 'AliceDevice2',
'methods': ['m.sas.v1'],
'timestamp': 1559598944869,
'transaction_id': 'S0meUniqueAndOpaqueString'
}); });
await keyVerification.handlePayload('m.key.verification.start', { await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
'from_device': 'BobDevice1', await Future.delayed(Duration(milliseconds: 10));
'method': 'm.sas.v1', await sub.cancel();
'transaction_id': 'S0meUniqueAndOpaqueString' expect(req2 != null, true);
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
// compare emoji
expect(req1.state, KeyVerificationState.askSas);
expect(req2.state, KeyVerificationState.askSas);
expect(req1.sasTypes[0], 'emoji');
expect(req1.sasTypes[1], 'decimal');
expect(req2.sasTypes[0], 'emoji');
expect(req2.sasTypes[1], 'decimal');
// compare emoji
final emoji1 = req1.sasEmojis;
final emoji2 = req2.sasEmojis;
for (var i = 0; i < 7; i++) {
expect(emoji1[i].emoji, emoji2[i].emoji);
expect(emoji1[i].name, emoji2[i].name);
}
// compare numbers
final numbers1 = req1.sasNumbers;
final numbers2 = req2.sasNumbers;
for (var i = 0; i < 3; i++) {
expect(numbers1[i], numbers2[i]);
}
// alright, they match
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req1.acceptSas();
evt = getLastSentEvent(req1);
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.waitingSas);
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptSas();
evt = getLastSentEvent(req2);
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.done);
expect(req2.state, KeyVerificationState.done);
expect(
client1.userDeviceKeys[client2.userID].deviceKeys[client2.deviceID]
.directVerified,
true);
expect(
client2.userDeviceKeys[client1.userID].deviceKeys[client1.deviceID]
.directVerified,
true);
await client1.encryption.keyVerificationManager.cleanup();
await client2.encryption.keyVerificationManager.cleanup();
});
test('ask SSSS start', () async {
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
await client1.database.clearSSSSCache(client1.id);
final req1 =
await client1.userDeviceKeys[client2.userID].startVerification();
expect(req1.state, KeyVerificationState.askSSSS);
await req1.openSSSS(recoveryKey: SSSS_KEY);
await Future.delayed(Duration(milliseconds: 10));
expect(req1.state, KeyVerificationState.waitingAccept);
await req1.cancel();
await client1.encryption.keyVerificationManager.cleanup();
});
test('ask SSSS end', () async {
FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
// the other one has to have their master key verified to trigger asking for ssss
client2.userDeviceKeys[client2.userID].masterKey.setDirectVerified(true);
final req1 =
await client1.userDeviceKeys[client2.userID].startVerification();
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
}); });
await keyVerification.handlePayload('m.key.verification.cancel', { await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
'code': 'm.user', await Future.delayed(Duration(milliseconds: 10));
'reason': 'User rejected the key verification request', await sub.cancel();
'transaction_id': 'S0meUniqueAndOpaqueString' expect(req2 != null, true);
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
// compare emoji
expect(req1.state, KeyVerificationState.askSas);
expect(req2.state, KeyVerificationState.askSas);
// compare emoji
final emoji1 = req1.sasEmojis;
final emoji2 = req2.sasEmojis;
for (var i = 0; i < 7; i++) {
expect(emoji1[i].emoji, emoji2[i].emoji);
expect(emoji1[i].name, emoji2[i].name);
}
// compare numbers
final numbers1 = req1.sasNumbers;
final numbers2 = req2.sasNumbers;
for (var i = 0; i < 3; i++) {
expect(numbers1[i], numbers2[i]);
}
// alright, they match
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(true);
await client1.database.clearSSSSCache(client1.id);
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req1.acceptSas();
evt = getLastSentEvent(req1);
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.waitingSas);
// send mac
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptSas();
evt = getLastSentEvent(req2);
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.askSSSS);
expect(req2.state, KeyVerificationState.done);
await req1.openSSSS(recoveryKey: SSSS_KEY);
await Future.delayed(Duration(milliseconds: 10));
expect(req1.state, KeyVerificationState.done);
client1.encryption.ssss = MockSSSS(client1.encryption);
(client1.encryption.ssss as MockSSSS).requestedSecrets = false;
await client1.database.clearSSSSCache(client1.id);
await req1.maybeRequestSSSSSecrets();
await Future.delayed(Duration(milliseconds: 10));
expect((client1.encryption.ssss as MockSSSS).requestedSecrets, true);
// delay for 12 seconds to be sure no other tests clear the ssss cache
await Future.delayed(Duration(seconds: 12));
await client1.encryption.keyVerificationManager.cleanup();
await client2.encryption.keyVerificationManager.cleanup();
});
test('reject verification', () async {
FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
final req1 =
await client1.userDeviceKeys[client2.userID].startVerification();
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
}); });
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
FakeMatrixApi.calledEndpoints.clear();
await req2.rejectVerification();
evt = getLastSentEvent(req2);
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error);
expect(req2.state, KeyVerificationState.error);
await client1.encryption.keyVerificationManager.cleanup();
await client2.encryption.keyVerificationManager.cleanup();
}); });
test('rejectSas', () async {
await keyVerification.rejectSas(); test('reject sas', () async {
FakeMatrixApi.calledEndpoints.clear();
// make sure our master key is *not* verified to not triger SSSS for now
client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
final req1 =
await client1.userDeviceKeys[client2.userID].startVerification();
var evt = getLastSentEvent(req1);
expect(req1.state, KeyVerificationState.waitingAccept);
KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
expect(req2 != null, true);
// send ready
FakeMatrixApi.calledEndpoints.clear();
await req2.acceptVerification();
evt = getLastSentEvent(req2);
expect(req2.state, KeyVerificationState.waitingAccept);
// send start
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send accept
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req1);
// send key
FakeMatrixApi.calledEndpoints.clear();
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
evt = getLastSentEvent(req2);
// receive last key
FakeMatrixApi.calledEndpoints.clear();
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
await req1.acceptSas();
FakeMatrixApi.calledEndpoints.clear();
await req2.rejectSas();
evt = getLastSentEvent(req2);
await client1.encryption.keyVerificationManager.handleEventUpdate(evt);
expect(req1.state, KeyVerificationState.error);
expect(req2.state, KeyVerificationState.error);
await client1.encryption.keyVerificationManager.cleanup();
await client2.encryption.keyVerificationManager.cleanup();
}); });
test('rejectVerification', () async {
await keyVerification.rejectVerification(); test('other device accepted', () async {
}); FakeMatrixApi.calledEndpoints.clear();
test('start', () async { // make sure our master key is *not* verified to not triger SSSS for now
await keyVerification.start(); client1.userDeviceKeys[client1.userID].masterKey.setDirectVerified(false);
}); final req1 =
test('verifyActivity', () async { await client1.userDeviceKeys[client2.userID].startVerification();
final verified = await keyVerification.verifyActivity(); var evt = getLastSentEvent(req1);
expect(verified, true); expect(req1.state, KeyVerificationState.waitingAccept);
keyVerification?.dispose();
KeyVerification req2;
final sub = client2.onKeyVerificationRequest.stream.listen((req) {
req2 = req;
});
await client2.encryption.keyVerificationManager.handleEventUpdate(evt);
await Future.delayed(Duration(milliseconds: 10));
await sub.cancel();
expect(req2 != null, true);
await client2.encryption.keyVerificationManager
.handleEventUpdate(EventUpdate(
content: {
'event_id': req2.transactionId,
'type': 'm.key.verification.ready',
'content': {
'methods': ['m.sas.v1'],
'from_device': 'SOMEOTHERDEVICE',
'm.relates_to': {
'rel_type': 'm.reference',
'event_id': req2.transactionId,
},
},
'origin_server_ts': DateTime.now().millisecondsSinceEpoch,
'sender': client2.userID,
},
eventType: 'm.key.verification.ready',
type: 'timeline',
roomID: req2.room.id,
));
expect(req2.state, KeyVerificationState.error);
await req2.cancel();
await client1.encryption.keyVerificationManager.cleanup();
await client2.encryption.keyVerificationManager.cleanup();
}); });
test('dispose client', () async { test('dispose client', () async {
await client.dispose(closeDatabase: true); await client1.dispose(closeDatabase: true);
await client2.dispose(closeDatabase: true);
}); });
}); });
} }

View file

@ -78,6 +78,11 @@ class FakeMatrixApi extends MockClient {
action.contains('/state/m.room.member/')) { action.contains('/state/m.room.member/')) {
res = {'displayname': ''}; res = {'displayname': ''};
return Response(json.encode(res), 200); return Response(json.encode(res), 200);
} else if (method == 'PUT' &&
action.contains(
'/client/r0/rooms/%211234%3AfakeServer.notExisting/send/')) {
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
return Response(json.encode(res), 200);
} else { } else {
res = { res = {
'errcode': 'M_UNRECOGNIZED', 'errcode': 'M_UNRECOGNIZED',
@ -2008,6 +2013,10 @@ class FakeMatrixApi extends MockClient {
(var req) => {}, (var req) => {},
'/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data': '/client/r0/user/%40alice%3Aexample.com/rooms/1234/account_data/test.account.data':
(var req) => {}, (var req) => {},
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/m.direct':
(var req) => {},
'/client/r0/user/%40othertest%3AfakeServer.notExisting/account_data/m.direct':
(var req) => {},
'/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {}, '/client/r0/profile/%40alice%3Aexample.com/displayname': (var reqI) => {},
'/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {}, '/client/r0/profile/%40alice%3Aexample.com/avatar_url': (var reqI) => {},
'/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url': '/client/r0/profile/%40test%3AfakeServer.notExisting/avatar_url':