Initial commit version 0.8.13
This commit is contained in:
commit
9526dfa4f2
111 changed files with 35074 additions and 0 deletions
275
test/encryption/bootstrap_test.dart
Normal file
275
test/encryption/bootstrap_test.dart
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
|
||||
void main() {
|
||||
group('Bootstrap', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
late Map<String, dynamic> oldSecret;
|
||||
late String origKeyId;
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
await client.abortSync();
|
||||
});
|
||||
|
||||
test('setup', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
Bootstrap? bootstrap;
|
||||
bootstrap = client.encryption!.bootstrap(
|
||||
onUpdate: () async {
|
||||
while (bootstrap == null) {
|
||||
await Future.delayed(Duration(milliseconds: 5));
|
||||
}
|
||||
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||
bootstrap.wipeSsss(true);
|
||||
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||
await bootstrap.newSsss('foxies');
|
||||
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||
bootstrap.wipeCrossSigning(true);
|
||||
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
|
||||
await bootstrap.askSetupCrossSigning(
|
||||
setupMasterKey: true,
|
||||
setupSelfSigningKey: true,
|
||||
setupUserSigningKey: true,
|
||||
);
|
||||
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||
bootstrap.wipeOnlineKeyBackup(true);
|
||||
} else if (bootstrap.state ==
|
||||
BootstrapState.askSetupOnlineKeyBackup) {
|
||||
await bootstrap.askSetupOnlineKeyBackup(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
while (bootstrap.state != BootstrapState.done) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
final defaultKey = client.encryption!.ssss.open();
|
||||
await defaultKey.unlock(passphrase: 'foxies');
|
||||
|
||||
// test all the x-signing keys match up
|
||||
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||
final privateKey = base64
|
||||
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
final pubKey = keyObj.init_with_seed(privateKey);
|
||||
expect(
|
||||
pubKey,
|
||||
client.userDeviceKeys[client.userID]
|
||||
?.getCrossSigningKey(keyType)
|
||||
?.publicKey);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
}
|
||||
|
||||
await defaultKey.store('foxes', 'floof');
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
oldSecret =
|
||||
json.decode(json.encode(client.accountData['foxes']!.content));
|
||||
origKeyId = defaultKey.keyId;
|
||||
}, timeout: Timeout(Duration(minutes: 2)));
|
||||
|
||||
test('change recovery passphrase', () async {
|
||||
if (!olmEnabled) return;
|
||||
Bootstrap? bootstrap;
|
||||
bootstrap = client.encryption!.bootstrap(
|
||||
onUpdate: () async {
|
||||
while (bootstrap == null) {
|
||||
await Future.delayed(Duration(milliseconds: 5));
|
||||
}
|
||||
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||
bootstrap.wipeSsss(false);
|
||||
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
|
||||
bootstrap.useExistingSsss(false);
|
||||
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
|
||||
await bootstrap.oldSsssKeys![client.encryption!.ssss.defaultKeyId]!
|
||||
.unlock(passphrase: 'foxies');
|
||||
bootstrap.unlockedSsss();
|
||||
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||
await bootstrap.newSsss('newfoxies');
|
||||
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||
bootstrap.wipeCrossSigning(false);
|
||||
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||
bootstrap.wipeOnlineKeyBackup(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
while (bootstrap.state != BootstrapState.done) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
final defaultKey = client.encryption!.ssss.open();
|
||||
await defaultKey.unlock(passphrase: 'newfoxies');
|
||||
|
||||
// test all the x-signing keys match up
|
||||
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||
final privateKey = base64
|
||||
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
final pubKey = keyObj.init_with_seed(privateKey);
|
||||
expect(
|
||||
pubKey,
|
||||
client.userDeviceKeys[client.userID]
|
||||
?.getCrossSigningKey(keyType)
|
||||
?.publicKey);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
}
|
||||
|
||||
expect(await defaultKey.getStored('foxes'), 'floof');
|
||||
}, timeout: Timeout(Duration(minutes: 2)));
|
||||
|
||||
test('change passphrase with multiple keys', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.setAccountData(client.userID!, 'foxes', oldSecret);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
|
||||
Bootstrap? bootstrap;
|
||||
bootstrap = client.encryption!.bootstrap(
|
||||
onUpdate: () async {
|
||||
while (bootstrap == null) {
|
||||
await Future.delayed(Duration(milliseconds: 5));
|
||||
}
|
||||
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||
bootstrap.wipeSsss(false);
|
||||
} else if (bootstrap.state == BootstrapState.askUseExistingSsss) {
|
||||
bootstrap.useExistingSsss(false);
|
||||
} else if (bootstrap.state == BootstrapState.askUnlockSsss) {
|
||||
await bootstrap.oldSsssKeys![client.encryption!.ssss.defaultKeyId]!
|
||||
.unlock(passphrase: 'newfoxies');
|
||||
await bootstrap.oldSsssKeys![origKeyId]!
|
||||
.unlock(passphrase: 'foxies');
|
||||
bootstrap.unlockedSsss();
|
||||
} else if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||
await bootstrap.newSsss('supernewfoxies');
|
||||
} else if (bootstrap.state == BootstrapState.askWipeCrossSigning) {
|
||||
bootstrap.wipeCrossSigning(false);
|
||||
} else if (bootstrap.state == BootstrapState.askWipeOnlineKeyBackup) {
|
||||
bootstrap.wipeOnlineKeyBackup(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
while (bootstrap.state != BootstrapState.done) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
final defaultKey = client.encryption!.ssss.open();
|
||||
await defaultKey.unlock(passphrase: 'supernewfoxies');
|
||||
|
||||
// test all the x-signing keys match up
|
||||
for (final keyType in {'master', 'user_signing', 'self_signing'}) {
|
||||
final privateKey = base64
|
||||
.decode(await defaultKey.getStored('m.cross_signing.$keyType'));
|
||||
final keyObj = olm.PkSigning();
|
||||
try {
|
||||
final pubKey = keyObj.init_with_seed(privateKey);
|
||||
expect(
|
||||
pubKey,
|
||||
client.userDeviceKeys[client.userID]
|
||||
?.getCrossSigningKey(keyType)
|
||||
?.publicKey);
|
||||
} finally {
|
||||
keyObj.free();
|
||||
}
|
||||
}
|
||||
|
||||
expect(await defaultKey.getStored('foxes'), 'floof');
|
||||
}, timeout: Timeout(Duration(minutes: 2)));
|
||||
|
||||
test('setup new ssss', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.accountData.clear();
|
||||
Bootstrap? bootstrap;
|
||||
bootstrap = client.encryption!.bootstrap(
|
||||
onUpdate: () async {
|
||||
while (bootstrap == null) {
|
||||
await Future.delayed(Duration(milliseconds: 5));
|
||||
}
|
||||
if (bootstrap.state == BootstrapState.askNewSsss) {
|
||||
await bootstrap.newSsss('thenewestfoxies');
|
||||
} else if (bootstrap.state == BootstrapState.askSetupCrossSigning) {
|
||||
await bootstrap.askSetupCrossSigning();
|
||||
} else if (bootstrap.state ==
|
||||
BootstrapState.askSetupOnlineKeyBackup) {
|
||||
await bootstrap.askSetupOnlineKeyBackup(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
while (bootstrap.state != BootstrapState.done) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
final defaultKey = client.encryption!.ssss.open();
|
||||
await defaultKey.unlock(passphrase: 'thenewestfoxies');
|
||||
}, timeout: Timeout(Duration(minutes: 2)));
|
||||
|
||||
test('bad ssss', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.accountData.clear();
|
||||
await client.setAccountData(client.userID!, 'foxes', oldSecret);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
var askedBadSsss = false;
|
||||
Bootstrap? bootstrap;
|
||||
bootstrap = client.encryption!.bootstrap(
|
||||
onUpdate: () async {
|
||||
while (bootstrap == null) {
|
||||
await Future.delayed(Duration(milliseconds: 5));
|
||||
}
|
||||
if (bootstrap.state == BootstrapState.askWipeSsss) {
|
||||
bootstrap.wipeSsss(false);
|
||||
} else if (bootstrap.state == BootstrapState.askBadSsss) {
|
||||
askedBadSsss = true;
|
||||
bootstrap.ignoreBadSecrets(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
while (bootstrap.state != BootstrapState.error) {
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
}
|
||||
expect(askedBadSsss, true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
122
test/encryption/cross_signing_test.dart
Normal file
122
test/encryption/cross_signing_test.dart
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Cross Signing', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
if (!olmEnabled) return;
|
||||
expect(client.encryption?.crossSigning.enabled, true);
|
||||
});
|
||||
|
||||
test('selfSign', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key = client.userDeviceKeys[client.userID]!.masterKey!;
|
||||
key.setDirectVerified(false);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.crossSigning.selfSign(recoveryKey: ssssKey);
|
||||
expect(key.directVerified, true);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints
|
||||
.containsKey('/client/r0/keys/signatures/upload'),
|
||||
true);
|
||||
expect(await client.encryption!.crossSigning.isCached(), true);
|
||||
});
|
||||
|
||||
test('signable', () async {
|
||||
if (!olmEnabled) return;
|
||||
expect(
|
||||
client.encryption!.crossSigning
|
||||
.signable([client.userDeviceKeys[client.userID!]!.masterKey!]),
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.crossSigning.signable([
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys[client.deviceID!]!
|
||||
]),
|
||||
false);
|
||||
expect(
|
||||
client.encryption!.crossSigning.signable([
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!
|
||||
]),
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.crossSigning.signable([
|
||||
client
|
||||
.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
|
||||
]),
|
||||
false);
|
||||
});
|
||||
|
||||
test('sign', () async {
|
||||
if (!olmEnabled) return;
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.crossSigning.sign([
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!,
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!,
|
||||
client.userDeviceKeys['@othertest:fakeServer.notExisting']!.masterKey!
|
||||
]);
|
||||
final body = json.decode(FakeMatrixApi
|
||||
.calledEndpoints['/client/r0/keys/signatures/upload']!.first);
|
||||
expect(body['@test:fakeServer.notExisting']?.containsKey('OTHERDEVICE'),
|
||||
true);
|
||||
expect(
|
||||
body['@test:fakeServer.notExisting'].containsKey(
|
||||
client.userDeviceKeys[client.userID]!.masterKey!.publicKey),
|
||||
true);
|
||||
expect(
|
||||
body['@othertest:fakeServer.notExisting'].containsKey(client
|
||||
.userDeviceKeys['@othertest:fakeServer.notExisting']
|
||||
?.masterKey
|
||||
?.publicKey),
|
||||
true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
107
test/encryption/encrypt_decrypt_room_message_test.dart
Normal file
107
test/encryption/encrypt_decrypt_room_message_test.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
|
||||
void main() {
|
||||
group('Encrypt/Decrypt room message', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
late Room room;
|
||||
late Map<String, dynamic> payload;
|
||||
final now = DateTime.now();
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
room = client.getRoomById(roomId)!;
|
||||
});
|
||||
|
||||
test('encrypt payload', () async {
|
||||
if (!olmEnabled) return;
|
||||
payload = await client.encryption!.encryptGroupMessagePayload(roomId, {
|
||||
'msgtype': 'm.text',
|
||||
'text': 'Hello foxies!',
|
||||
});
|
||||
expect(payload['algorithm'], AlgorithmTypes.megolmV1AesSha2);
|
||||
expect(payload['ciphertext'] is String, true);
|
||||
expect(payload['device_id'], client.deviceID);
|
||||
expect(payload['sender_key'], client.identityKey);
|
||||
expect(payload['session_id'] is String, true);
|
||||
});
|
||||
|
||||
test('decrypt payload', () async {
|
||||
if (!olmEnabled) return;
|
||||
final encryptedEvent = Event(
|
||||
type: EventTypes.Encrypted,
|
||||
content: payload,
|
||||
room: room,
|
||||
originServerTs: now,
|
||||
eventId: '\$event',
|
||||
senderId: client.userID!,
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption!.decryptRoomEvent(roomId, encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.room.message');
|
||||
expect(decryptedEvent.content['msgtype'], 'm.text');
|
||||
expect(decryptedEvent.content['text'], 'Hello foxies!');
|
||||
});
|
||||
|
||||
test('decrypt payload nocache', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
final encryptedEvent = Event(
|
||||
type: EventTypes.Encrypted,
|
||||
content: payload,
|
||||
room: room,
|
||||
originServerTs: now,
|
||||
eventId: '\$event',
|
||||
senderId: '@alice:example.com',
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption!.decryptRoomEvent(roomId, encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.room.message');
|
||||
expect(decryptedEvent.content['msgtype'], 'm.text');
|
||||
expect(decryptedEvent.content['text'], 'Hello foxies!');
|
||||
await client.encryption!
|
||||
.decryptRoomEvent(roomId, encryptedEvent, store: true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
124
test/encryption/encrypt_decrypt_to_device_test.dart
Normal file
124
test/encryption/encrypt_decrypt_to_device_test.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_database.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
// key @othertest:fakeServer.notExisting
|
||||
const otherPickledOlmAccount =
|
||||
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
|
||||
|
||||
group('Encrypt/Decrypt to-device messages', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
final otherClient = Client('othertestclient',
|
||||
httpClient: FakeMatrixApi(), databaseBuilder: getDatabase);
|
||||
late DeviceKeys device;
|
||||
late Map<String, dynamic> payload;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
await client.abortSync();
|
||||
await otherClient.checkHomeserver('https://fakeserver.notexisting',
|
||||
checkWellKnown: false);
|
||||
await otherClient.init(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: otherClient.homeserver,
|
||||
newDeviceName: 'Text Matrix Client',
|
||||
newDeviceID: 'FOXDEVICE',
|
||||
newOlmAccount: otherPickledOlmAccount,
|
||||
);
|
||||
await otherClient.abortSync();
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
device = DeviceKeys.fromJson({
|
||||
'user_id': client.userID,
|
||||
'device_id': client.deviceID,
|
||||
'algorithms': [
|
||||
AlgorithmTypes.olmV1Curve25519AesSha2,
|
||||
AlgorithmTypes.megolmV1AesSha2
|
||||
],
|
||||
'keys': {
|
||||
'curve25519:${client.deviceID}': client.identityKey,
|
||||
'ed25519:${client.deviceID}': client.fingerprintKey,
|
||||
},
|
||||
}, client);
|
||||
});
|
||||
|
||||
test('encryptToDeviceMessage', () async {
|
||||
if (!olmEnabled) return;
|
||||
payload = await otherClient.encryption!
|
||||
.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'});
|
||||
});
|
||||
|
||||
test('decryptToDeviceEvent', () async {
|
||||
if (!olmEnabled) return;
|
||||
final encryptedEvent = ToDeviceEvent(
|
||||
sender: '@othertest:fakeServer.notExisting',
|
||||
type: EventTypes.Encrypted,
|
||||
content: payload[client.userID][client.deviceID],
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption!.decryptToDeviceEvent(encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.to_device');
|
||||
expect(decryptedEvent.content['hello'], 'foxies');
|
||||
});
|
||||
|
||||
test('decryptToDeviceEvent nocache', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.encryption!.olmManager.olmSessions.clear();
|
||||
payload = await otherClient.encryption!.encryptToDeviceMessage(
|
||||
[device], 'm.to_device', {'hello': 'superfoxies'});
|
||||
final encryptedEvent = ToDeviceEvent(
|
||||
sender: '@othertest:fakeServer.notExisting',
|
||||
type: EventTypes.Encrypted,
|
||||
content: payload[client.userID][client.deviceID],
|
||||
);
|
||||
final decryptedEvent =
|
||||
await client.encryption!.decryptToDeviceEvent(encryptedEvent);
|
||||
expect(decryptedEvent.type, 'm.to_device');
|
||||
expect(decryptedEvent.content['hello'], 'superfoxies');
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
await otherClient.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
577
test/encryption/key_manager_test.dart
Normal file
577
test/encryption/key_manager_test.dart
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Key Manager', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('handle new m.room_key', () async {
|
||||
if (!olmEnabled) return;
|
||||
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
final sessionKey =
|
||||
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw';
|
||||
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
var event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': validSenderKey,
|
||||
});
|
||||
await client.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
client.encryption!.keyManager.getInboundGroupSession(
|
||||
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
|
||||
null,
|
||||
true);
|
||||
|
||||
// now test a few invalid scenarios
|
||||
|
||||
// not encrypted
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
});
|
||||
await client.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
client.encryption!.keyManager.getInboundGroupSession(
|
||||
'!726s6s6q:example.com', validSessionId, validSenderKey) !=
|
||||
null,
|
||||
false);
|
||||
});
|
||||
|
||||
test('outbound group session', () async {
|
||||
if (!olmEnabled) return;
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
var sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
var inbound = client.encryption!.keyManager.getInboundGroupSession(
|
||||
roomId, sess.outboundGroupSession!.session_id(), client.identityKey);
|
||||
expect(inbound != null, true);
|
||||
expect(
|
||||
inbound!.allowedAtIndex['@alice:example.com']
|
||||
?['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
|
||||
0);
|
||||
expect(
|
||||
inbound.allowedAtIndex['@alice:example.com']
|
||||
?['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
|
||||
0);
|
||||
|
||||
// rotate after too many messages
|
||||
Iterable.generate(300).forEach((_) {
|
||||
sess.outboundGroupSession!.encrypt('some string');
|
||||
});
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
|
||||
// rotate if device is blocked
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
|
||||
.blocked = true;
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
|
||||
.blocked = false;
|
||||
|
||||
// lazy-create if it would rotate
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
final oldSessKey = sess.outboundGroupSession!.session_key();
|
||||
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
|
||||
.blocked = true;
|
||||
await client.encryption!.keyManager.prepareOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getOutboundGroupSession(roomId)!
|
||||
.outboundGroupSession!
|
||||
.session_key() !=
|
||||
oldSessKey,
|
||||
true);
|
||||
client.userDeviceKeys['@alice:example.com']!.deviceKeys['JLAFKJWSCS']!
|
||||
.blocked = false;
|
||||
|
||||
// rotate if too far in the past
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
sess.creationTime = DateTime.now().subtract(Duration(days: 30));
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
|
||||
// rotate if user leaves
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
final room = client.getRoomById(roomId)!;
|
||||
final member = room.getState('m.room.member', '@alice:example.com');
|
||||
member!.content['membership'] = 'leave';
|
||||
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! - 1;
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
member.content['membership'] = 'join';
|
||||
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! + 1;
|
||||
|
||||
// do not rotate if new device is added
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
sess.outboundGroupSession!.encrypt(
|
||||
'foxies'); // so that the new device will have a different index
|
||||
client.userDeviceKeys['@alice:example.com']?.deviceKeys['NEWDEVICE'] =
|
||||
DeviceKeys.fromJson({
|
||||
'user_id': '@alice:example.com',
|
||||
'device_id': 'NEWDEVICE',
|
||||
'algorithms': [
|
||||
AlgorithmTypes.olmV1Curve25519AesSha2,
|
||||
AlgorithmTypes.megolmV1AesSha2
|
||||
],
|
||||
'keys': {
|
||||
'curve25519:NEWDEVICE': 'bnKQp6pPW0l9cGoIgHpBoK5OUi4h0gylJ7upc4asFV8',
|
||||
'ed25519:NEWDEVICE': 'ZZhPdvWYg3MRpGy2MwtI+4MHXe74wPkBli5hiEOUi8Y'
|
||||
},
|
||||
'signatures': {
|
||||
'@alice:example.com': {
|
||||
'ed25519:NEWDEVICE':
|
||||
'94GSg8N9vNB8wyWHJtKaaX3MGNWPVOjBatJM+TijY6B1RlDFJT5Cl1h/tjr17AoQz0CDdOf6uFhrYsBkH1/ABg'
|
||||
}
|
||||
}
|
||||
}, client);
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
inbound = client.encryption!.keyManager.getInboundGroupSession(
|
||||
roomId, sess.outboundGroupSession!.session_id(), client.identityKey);
|
||||
expect(
|
||||
inbound!.allowedAtIndex['@alice:example.com']
|
||||
?['L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8'],
|
||||
0);
|
||||
expect(
|
||||
inbound.allowedAtIndex['@alice:example.com']
|
||||
?['wMIDhiQl5jEXQrTB03ePOSQfR8sA/KMrW0CIfFfXKEE'],
|
||||
0);
|
||||
expect(
|
||||
inbound.allowedAtIndex['@alice:example.com']
|
||||
?['bnKQp6pPW0l9cGoIgHpBoK5OUi4h0gylJ7upc4asFV8'],
|
||||
1);
|
||||
|
||||
// do not rotate if new user is added
|
||||
member.content['membership'] = 'leave';
|
||||
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! - 1;
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
member.content['membership'] = 'join';
|
||||
room.summary.mJoinedMemberCount = room.summary.mJoinedMemberCount! + 1;
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
|
||||
// force wipe
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
await client.encryption!.keyManager
|
||||
.clearOrUseOutboundGroupSession(roomId, wipe: true);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
|
||||
// load from database
|
||||
sess = await client.encryption!.keyManager
|
||||
.createOutboundGroupSession(roomId);
|
||||
client.encryption!.keyManager.clearOutboundGroupSessions();
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
false);
|
||||
await client.encryption!.keyManager.loadOutboundGroupSession(roomId);
|
||||
expect(
|
||||
client.encryption!.keyManager.getOutboundGroupSession(roomId) != null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('inbound group session', () async {
|
||||
if (!olmEnabled) return;
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
final sessionContent = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU',
|
||||
'session_key':
|
||||
'AgAAAAAQcQ6XrFJk6Prm8FikZDqfry/NbDz8Xw7T6e+/9Yf/q3YHIPEQlzv7IZMNcYb51ifkRzFejVvtphS7wwG2FaXIp4XS2obla14iKISR0X74ugB2vyb1AydIHE/zbBQ1ic5s3kgjMFlWpu/S3FQCnCrv+DPFGEt3ERGWxIl3Bl5X53IjPyVkz65oljz2TZESwz0GH/QFvyOOm8ci0q/gceaF3S7Dmafg3dwTKYwcA5xkcc+BLyrLRzB6Hn+oMAqSNSscnm4mTeT5zYibIhrzqyUTMWr32spFtI9dNR/RFSzfCw'
|
||||
};
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
false);
|
||||
client.encryption!.keyManager
|
||||
.setInboundGroupSession(roomId, sessionId, senderKey, sessionContent);
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession('otherroom', sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession('otherroom', sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession('otherroom', 'invalid', senderKey) !=
|
||||
null,
|
||||
false);
|
||||
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
false);
|
||||
await client.encryption!.keyManager
|
||||
.loadInboundGroupSession(roomId, sessionId, senderKey);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
false);
|
||||
await client.encryption!.keyManager
|
||||
.loadInboundGroupSession(roomId, sessionId, 'invalid');
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, 'invalid') !=
|
||||
null,
|
||||
false);
|
||||
});
|
||||
|
||||
test('setInboundGroupSession', () async {
|
||||
if (!olmEnabled) return;
|
||||
final session = olm.OutboundGroupSession();
|
||||
session.create();
|
||||
final inbound = olm.InboundGroupSession();
|
||||
inbound.create(session.session_key());
|
||||
final senderKey = client.identityKey;
|
||||
final roomId = '!someroom:example.org';
|
||||
final sessionId = inbound.session_id();
|
||||
final room = Room(id: roomId, client: client);
|
||||
client.rooms.add(room);
|
||||
// we build up an encrypted message so that we can test if it successfully decrypted afterwards
|
||||
room.setState(
|
||||
Event(
|
||||
senderId: '@test:example.com',
|
||||
type: 'm.room.encrypted',
|
||||
room: room,
|
||||
eventId: '12345',
|
||||
originServerTs: DateTime.now(),
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'ciphertext': session.encrypt(json.encode({
|
||||
'type': 'm.room.message',
|
||||
'content': {'msgtype': 'm.text', 'body': 'foxies'},
|
||||
})),
|
||||
'device_id': client.deviceID,
|
||||
'sender_key': client.identityKey,
|
||||
'session_id': sessionId,
|
||||
},
|
||||
stateKey: '',
|
||||
),
|
||||
);
|
||||
expect(room.lastEvent?.type, 'm.room.encrypted');
|
||||
// set a payload...
|
||||
var sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(1),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.inboundGroupSession
|
||||
?.first_known_index(),
|
||||
1);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.forwardingCurve25519KeyChain
|
||||
.length,
|
||||
1);
|
||||
|
||||
// not set one with a higher first known index
|
||||
sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(2),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.inboundGroupSession
|
||||
?.first_known_index(),
|
||||
1);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.forwardingCurve25519KeyChain
|
||||
.length,
|
||||
1);
|
||||
|
||||
// set one with a lower first known index
|
||||
sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(0),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.inboundGroupSession
|
||||
?.first_known_index(),
|
||||
0);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.forwardingCurve25519KeyChain
|
||||
.length,
|
||||
1);
|
||||
|
||||
// not set one with a longer forwarding chain
|
||||
sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey, 'beep'],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(0),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.inboundGroupSession
|
||||
?.first_known_index(),
|
||||
0);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.forwardingCurve25519KeyChain
|
||||
.length,
|
||||
1);
|
||||
|
||||
// set one with a shorter forwarding chain
|
||||
sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(0),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.inboundGroupSession
|
||||
?.first_known_index(),
|
||||
0);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey)
|
||||
?.forwardingCurve25519KeyChain
|
||||
.length,
|
||||
0);
|
||||
|
||||
// test that it decrypted the last event
|
||||
expect(room.lastEvent?.type, 'm.room.message');
|
||||
expect(room.lastEvent?.content['body'], 'foxies');
|
||||
|
||||
inbound.free();
|
||||
session.free();
|
||||
});
|
||||
|
||||
test('Reused deviceID attack', () async {
|
||||
if (!olmEnabled) return;
|
||||
Logs().level = Level.warning;
|
||||
|
||||
// Ensure the device came from sync
|
||||
expect(
|
||||
client.userDeviceKeys['@alice:example.com']
|
||||
?.deviceKeys['JLAFKJWSCS'] !=
|
||||
null,
|
||||
true);
|
||||
|
||||
// Alice removes her device
|
||||
client.userDeviceKeys['@alice:example.com']?.deviceKeys
|
||||
.remove('JLAFKJWSCS');
|
||||
|
||||
// Alice adds her device with same device ID but different keys
|
||||
final oldResp = FakeMatrixApi.api['POST']?['/client/r0/keys/query'](null);
|
||||
FakeMatrixApi.api['POST']?['/client/r0/keys/query'] = (_) {
|
||||
oldResp['device_keys']['@alice:example.com']['JLAFKJWSCS'] = {
|
||||
'user_id': '@alice:example.com',
|
||||
'device_id': 'JLAFKJWSCS',
|
||||
'algorithms': [
|
||||
'm.olm.v1.curve25519-aes-sha2',
|
||||
'm.megolm.v1.aes-sha2'
|
||||
],
|
||||
'keys': {
|
||||
'curve25519:JLAFKJWSCS':
|
||||
'WbwrNyD7nvtmcLQ0TTuVPFGJq6JznfjrVsjIpmBqvDw',
|
||||
'ed25519:JLAFKJWSCS': 'vl0d54pTVRcvBgUzoQFa8e6TldHWG9O8bh0iuIvgd/I'
|
||||
},
|
||||
'signatures': {
|
||||
'@alice:example.com': {
|
||||
'ed25519:JLAFKJWSCS':
|
||||
's/L86jLa8BTroL8GsBeqO0gRLC3ZrSA7Gch6UoLI2SefC1+1ycmnP9UGbLPh3qBJOmlhczMpBLZwelg87qNNDA'
|
||||
}
|
||||
}
|
||||
};
|
||||
return oldResp;
|
||||
};
|
||||
client.userDeviceKeys['@alice:example.com']!.outdated = true;
|
||||
await client.updateUserDeviceKeys();
|
||||
expect(
|
||||
client.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS'],
|
||||
null);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: false);
|
||||
});
|
||||
});
|
||||
}
|
||||
404
test/encryption/key_request_test.dart
Normal file
404
test/encryption/key_request_test.dart
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
Map<String, dynamic> jsonDecode(dynamic payload) {
|
||||
if (payload is String) {
|
||||
try {
|
||||
return json.decode(payload);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (payload is Map<String, dynamic>) return payload;
|
||||
return {};
|
||||
}
|
||||
|
||||
void main() {
|
||||
/// All Tests related to device keys
|
||||
group('Key Request', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final validSenderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
test('Create Request', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
final matrix = await getClient();
|
||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com')!;
|
||||
await matrix.encryption!.keyManager.request(
|
||||
requestRoom, 'sessionId', validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
var foundEvent = false;
|
||||
for (final entry in FakeMatrixApi.calledEndpoints.entries) {
|
||||
final payload = jsonDecode(entry.value.first);
|
||||
if (entry.key
|
||||
.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
|
||||
(payload['messages'] is Map) &&
|
||||
(payload['messages']['@alice:example.com'] is Map) &&
|
||||
(payload['messages']['@alice:example.com']['*'] is Map)) {
|
||||
final content = payload['messages']['@alice:example.com']['*'];
|
||||
if (content['action'] == 'request' &&
|
||||
content['body']['room_id'] == '!726s6s6q:example.com' &&
|
||||
content['body']['sender_key'] == validSenderKey &&
|
||||
content['body']['session_id'] == 'sessionId') {
|
||||
foundEvent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(foundEvent, true);
|
||||
await matrix.dispose(closeDatabase: true);
|
||||
});
|
||||
test('Reply To Request', () async {
|
||||
if (!olmEnabled) return;
|
||||
final matrix = await getClient();
|
||||
matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await matrix
|
||||
.userDeviceKeys['@alice:example.com']!.deviceKeys['OTHERDEVICE']!
|
||||
.setBlocked(false);
|
||||
await matrix
|
||||
.userDeviceKeys['@alice:example.com']!.deviceKeys['OTHERDEVICE']!
|
||||
.setVerified(true);
|
||||
final session = await matrix.encryption!.keyManager
|
||||
.loadInboundGroupSession(
|
||||
'!726s6s6q:example.com', validSessionId, validSenderKey);
|
||||
// test a successful share
|
||||
var event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_1',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
|
||||
// test a successful foreign share
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
session!.allowedAtIndex['@test:fakeServer.notExisting'] = <String, int>{
|
||||
matrix.userDeviceKeys['@test:fakeServer.notExisting']!
|
||||
.deviceKeys['OTHERDEVICE']!.curve25519Key!: 0,
|
||||
};
|
||||
event = ToDeviceEvent(
|
||||
sender: '@test:fakeServer.notExisting',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_a1',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
session.allowedAtIndex.remove('@test:fakeServer.notExisting');
|
||||
|
||||
// test various fail scenarios
|
||||
|
||||
// unknown person
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@test:fakeServer.notExisting',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_a2',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
Logs().i(FakeMatrixApi.calledEndpoints.keys.toString());
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// no body
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'request_id': 'request_2',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// request by ourself
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_3',
|
||||
'requesting_device_id': 'JLAFKJWSCS',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// device not found
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_4',
|
||||
'requesting_device_id': 'blubb',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// unknown room
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!invalid:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': validSessionId,
|
||||
},
|
||||
'request_id': 'request_5',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// unknwon session
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.room_key_request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'body': {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'sender_key': validSenderKey,
|
||||
'session_id': 'invalid',
|
||||
},
|
||||
'request_id': 'request_6',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await matrix.dispose(closeDatabase: true);
|
||||
});
|
||||
test('Receive shared keys', () async {
|
||||
if (!olmEnabled) return;
|
||||
final matrix = await getClient();
|
||||
final requestRoom = matrix.getRoomById('!726s6s6q:example.com')!;
|
||||
await matrix.encryption!.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
|
||||
final session = (await matrix.encryption!.keyManager
|
||||
.loadInboundGroupSession(
|
||||
requestRoom.id, validSessionId, validSenderKey))!;
|
||||
final sessionKey = session.inboundGroupSession!
|
||||
.export_session(session.inboundGroupSession!.first_known_index());
|
||||
matrix.encryption!.keyManager.clearInboundGroupSessions();
|
||||
var event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.forwarded_room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
'sender_key': validSenderKey,
|
||||
'forwarding_curve25519_key_chain': [],
|
||||
'sender_claimed_ed25519_key':
|
||||
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
matrix.encryption!.keyManager.getInboundGroupSession(
|
||||
requestRoom.id, validSessionId, validSenderKey) !=
|
||||
null,
|
||||
true);
|
||||
|
||||
// now test a few invalid scenarios
|
||||
|
||||
// request not found
|
||||
matrix.encryption!.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.forwarded_room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
'sender_key': validSenderKey,
|
||||
'forwarding_curve25519_key_chain': [],
|
||||
'sender_claimed_ed25519_key':
|
||||
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
matrix.encryption!.keyManager.getInboundGroupSession(
|
||||
requestRoom.id, validSessionId, validSenderKey) !=
|
||||
null,
|
||||
false);
|
||||
|
||||
// unknown device
|
||||
await matrix.encryption!.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
matrix.encryption!.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.forwarded_room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
'sender_key': validSenderKey,
|
||||
'forwarding_curve25519_key_chain': [],
|
||||
'sender_claimed_ed25519_key':
|
||||
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
matrix.encryption!.keyManager.getInboundGroupSession(
|
||||
requestRoom.id, validSessionId, validSenderKey) !=
|
||||
null,
|
||||
false);
|
||||
|
||||
// no encrypted content
|
||||
await matrix.encryption!.keyManager.request(
|
||||
requestRoom, validSessionId, validSenderKey,
|
||||
tryOnlineBackup: false);
|
||||
matrix.encryption!.keyManager.clearInboundGroupSessions();
|
||||
event = ToDeviceEvent(
|
||||
sender: '@alice:example.com',
|
||||
type: 'm.forwarded_room_key',
|
||||
content: {
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': '!726s6s6q:example.com',
|
||||
'session_id': validSessionId,
|
||||
'session_key': sessionKey,
|
||||
'sender_key': validSenderKey,
|
||||
'forwarding_curve25519_key_chain': [],
|
||||
'sender_claimed_ed25519_key':
|
||||
'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8',
|
||||
});
|
||||
await matrix.encryption!.keyManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
matrix.encryption!.keyManager.getInboundGroupSession(
|
||||
requestRoom.id, validSessionId, validSenderKey) !=
|
||||
null,
|
||||
false);
|
||||
|
||||
// There is a non awaiting setInboundGroupSession call on the database
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
await matrix.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
486
test/encryption/key_verification_test.dart
Normal file
486
test/encryption/key_verification_test.dart
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_database.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();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
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,
|
||||
},
|
||||
type: EventUpdateType.timeline,
|
||||
roomID: req.room!.id,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
/// All Tests related to the ChatTime
|
||||
group('Key Verification', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
// key @othertest:fakeServer.notExisting
|
||||
const otherPickledOlmAccount =
|
||||
'VWhVApbkcilKAEGppsPDf9nNVjaK8/IxT3asSR0sYg0S5KgbfE8vXEPwoiKBX2cEvwX3OessOBOkk+ZE7TTbjlrh/KEd31p8Wo+47qj0AP+Ky+pabnhi+/rTBvZy+gfzTqUfCxZrkzfXI9Op4JnP6gYmy7dVX2lMYIIs9WCO1jcmIXiXum5jnfXu1WLfc7PZtO2hH+k9CDKosOFaXRBmsu8k/BGXPSoWqUpvu6WpEG9t5STk4FeAzA';
|
||||
|
||||
late Client client1;
|
||||
late Client client2;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client1 = await getClient();
|
||||
client2 = Client(
|
||||
'othertestclient',
|
||||
httpClient: FakeMatrixApi(),
|
||||
databaseBuilder: getDatabase,
|
||||
);
|
||||
await client2.checkHomeserver('https://fakeserver.notexisting',
|
||||
checkWellKnown: false);
|
||||
await client2.init(
|
||||
newToken: 'abc',
|
||||
newUserID: '@othertest:fakeServer.notExisting',
|
||||
newHomeserver: client2.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('Run emoji / number verification', () async {
|
||||
if (!olmEnabled) return;
|
||||
// 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
|
||||
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(
|
||||
newDirectChatEnableEncryption: false,
|
||||
);
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
late 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(
|
||||
client2.encryption!.keyVerificationManager
|
||||
.getRequest(req2.transactionId!),
|
||||
req2);
|
||||
|
||||
// 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 {
|
||||
if (!olmEnabled) return;
|
||||
client1.userDeviceKeys[client1.userID]!.masterKey!
|
||||
.setDirectVerified(true);
|
||||
await client1.encryption!.ssss.clearCache();
|
||||
final req1 = await client1.userDeviceKeys[client2.userID]!
|
||||
.startVerification(newDirectChatEnableEncryption: false);
|
||||
expect(req1.state, KeyVerificationState.askSSSS);
|
||||
await req1.openSSSS(recoveryKey: ssssKey);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
await req1.cancel();
|
||||
await client1.encryption!.keyVerificationManager.cleanup();
|
||||
});
|
||||
|
||||
test('ask SSSS end', () async {
|
||||
if (!olmEnabled) return;
|
||||
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(newDirectChatEnableEncryption: false);
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
late 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();
|
||||
|
||||
// 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.encryption!.ssss.clearCache();
|
||||
|
||||
// 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: ssssKey);
|
||||
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.encryption!.ssss.clearCache();
|
||||
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 {
|
||||
if (!olmEnabled) return;
|
||||
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(newDirectChatEnableEncryption: false);
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
late 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('reject sas', () async {
|
||||
if (!olmEnabled) return;
|
||||
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(newDirectChatEnableEncryption: false);
|
||||
var evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
late 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();
|
||||
|
||||
// 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('other device accepted', () async {
|
||||
if (!olmEnabled) return;
|
||||
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(newDirectChatEnableEncryption: false);
|
||||
final evt = getLastSentEvent(req1);
|
||||
expect(req1.state, KeyVerificationState.waitingAccept);
|
||||
|
||||
late 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();
|
||||
|
||||
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,
|
||||
},
|
||||
type: EventUpdateType.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 {
|
||||
if (!olmEnabled) return;
|
||||
await client1.dispose(closeDatabase: true);
|
||||
await client2.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
264
test/encryption/olm_manager_test.dart
Normal file
264
test/encryption/olm_manager_test.dart
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
import 'package:matrix/encryption/utils/json_signature_check_extension.dart';
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Olm Manager', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('signatures', () async {
|
||||
if (!olmEnabled) return;
|
||||
final payload = <String, dynamic>{
|
||||
'fox': 'floof',
|
||||
};
|
||||
final signedPayload = client.encryption!.olmManager.signJson(payload);
|
||||
expect(
|
||||
signedPayload.checkJsonSignature(
|
||||
client.fingerprintKey, client.userID!, client.deviceID!),
|
||||
true);
|
||||
});
|
||||
|
||||
test('uploadKeys', () async {
|
||||
if (!olmEnabled) return;
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
final res = await client.encryption!.olmManager
|
||||
.uploadKeys(uploadDeviceKeys: true);
|
||||
expect(res, true);
|
||||
var sent = json.decode(
|
||||
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
|
||||
expect(sent['device_keys'] != null, true);
|
||||
expect(sent['one_time_keys'] != null, true);
|
||||
expect(sent['one_time_keys'].keys.length, 66);
|
||||
expect(sent['fallback_keys'] != null, true);
|
||||
expect(sent['fallback_keys'].keys.length, 1);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.olmManager.uploadKeys();
|
||||
sent = json.decode(
|
||||
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
|
||||
expect(sent['device_keys'] != null, false);
|
||||
expect(sent['fallback_keys'].keys.length, 1);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.olmManager
|
||||
.uploadKeys(oldKeyCount: 20, unusedFallbackKey: true);
|
||||
sent = json.decode(
|
||||
FakeMatrixApi.calledEndpoints['/client/r0/keys/upload']!.first);
|
||||
expect(sent['one_time_keys'].keys.length, 46);
|
||||
expect(sent['fallback_keys'].keys.length, 0);
|
||||
});
|
||||
|
||||
test('handleDeviceOneTimeKeysCount', () async {
|
||||
if (!olmEnabled) return;
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption!.olmManager
|
||||
.handleDeviceOneTimeKeysCount({'signed_curve25519': 20}, null);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||
true);
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption!.olmManager
|
||||
.handleDeviceOneTimeKeysCount({'signed_curve25519': 70}, null);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||
false);
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption!.olmManager.handleDeviceOneTimeKeysCount(null, []);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||
true);
|
||||
|
||||
// this will upload keys because we assume the key count is 0, if the server doesn't send one
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption!.olmManager
|
||||
.handleDeviceOneTimeKeysCount(null, ['signed_curve25519']);
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.containsKey('/client/r0/keys/upload'),
|
||||
true);
|
||||
});
|
||||
|
||||
test('restoreOlmSession', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.encryption!.olmManager.olmSessions.clear();
|
||||
await client.encryption!.olmManager
|
||||
.restoreOlmSession(client.userID!, client.identityKey);
|
||||
expect(client.encryption!.olmManager.olmSessions.length, 1);
|
||||
|
||||
client.encryption!.olmManager.olmSessions.clear();
|
||||
await client.encryption!.olmManager
|
||||
.restoreOlmSession(client.userID!, 'invalid');
|
||||
expect(client.encryption!.olmManager.olmSessions.length, 0);
|
||||
|
||||
client.encryption!.olmManager.olmSessions.clear();
|
||||
await client.encryption!.olmManager
|
||||
.restoreOlmSession('invalid', client.identityKey);
|
||||
expect(client.encryption!.olmManager.olmSessions.length, 0);
|
||||
});
|
||||
|
||||
test('startOutgoingOlmSessions', () async {
|
||||
if (!olmEnabled) return;
|
||||
// start an olm session.....with ourself!
|
||||
client.encryption!.olmManager.olmSessions.clear();
|
||||
await client.encryption!.olmManager.startOutgoingOlmSessions([
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys[client.deviceID]!
|
||||
]);
|
||||
expect(
|
||||
client.encryption!.olmManager.olmSessions
|
||||
.containsKey(client.identityKey),
|
||||
true);
|
||||
});
|
||||
|
||||
test('replay to_device events', () async {
|
||||
if (!olmEnabled) return;
|
||||
final userId = '@alice:example.com';
|
||||
final deviceId = 'JLAFKJWSCS';
|
||||
final senderKey = 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8';
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.database!.setLastSentMessageUserDeviceKey(
|
||||
json.encode({
|
||||
'type': 'm.foxies',
|
||||
'content': {
|
||||
'floof': 'foxhole',
|
||||
},
|
||||
}),
|
||||
userId,
|
||||
deviceId);
|
||||
var event = ToDeviceEvent(
|
||||
sender: userId,
|
||||
type: 'm.dummy',
|
||||
content: {},
|
||||
encryptedContent: {
|
||||
'sender_key': senderKey,
|
||||
},
|
||||
);
|
||||
await client.encryption!.olmManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
|
||||
// fail scenarios
|
||||
|
||||
// not encrypted
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.database!.setLastSentMessageUserDeviceKey(
|
||||
json.encode({
|
||||
'type': 'm.foxies',
|
||||
'content': {
|
||||
'floof': 'foxhole',
|
||||
},
|
||||
}),
|
||||
userId,
|
||||
deviceId);
|
||||
event = ToDeviceEvent(
|
||||
sender: userId,
|
||||
type: 'm.dummy',
|
||||
content: {},
|
||||
encryptedContent: null,
|
||||
);
|
||||
await client.encryption!.olmManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// device not found
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.database!.setLastSentMessageUserDeviceKey(
|
||||
json.encode({
|
||||
'type': 'm.foxies',
|
||||
'content': {
|
||||
'floof': 'foxhole',
|
||||
},
|
||||
}),
|
||||
userId,
|
||||
deviceId);
|
||||
event = ToDeviceEvent(
|
||||
sender: userId,
|
||||
type: 'm.dummy',
|
||||
content: {},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.olmManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// don't replay if the last event is m.dummy itself
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.database!.setLastSentMessageUserDeviceKey(
|
||||
json.encode({
|
||||
'type': 'm.dummy',
|
||||
'content': {},
|
||||
}),
|
||||
userId,
|
||||
deviceId);
|
||||
event = ToDeviceEvent(
|
||||
sender: userId,
|
||||
type: 'm.dummy',
|
||||
content: {},
|
||||
encryptedContent: {
|
||||
'sender_key': senderKey,
|
||||
},
|
||||
);
|
||||
await client.encryption!.olmManager.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
124
test/encryption/online_key_backup_test.dart
Normal file
124
test/encryption/online_key_backup_test.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Online Key Backup', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
|
||||
final roomId = '!726s6s6q:example.com';
|
||||
final sessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
|
||||
final senderKey = 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg';
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
if (!olmEnabled) return;
|
||||
expect(client.encryption!.keyManager.enabled, true);
|
||||
expect(await client.encryption!.keyManager.isCached(), false);
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
await handle.maybeCacheAll();
|
||||
expect(await client.encryption!.keyManager.isCached(), true);
|
||||
});
|
||||
|
||||
test('load key', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
await client.encryption!.keyManager
|
||||
.request(client.getRoomById(roomId)!, sessionId, senderKey);
|
||||
expect(
|
||||
client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey) !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('upload key', () async {
|
||||
if (!olmEnabled) return;
|
||||
final session = olm.OutboundGroupSession();
|
||||
session.create();
|
||||
final inbound = olm.InboundGroupSession();
|
||||
inbound.create(session.session_key());
|
||||
final senderKey = client.identityKey;
|
||||
final roomId = '!someroom:example.org';
|
||||
final sessionId = inbound.session_id();
|
||||
// set a payload...
|
||||
final sessionPayload = <String, dynamic>{
|
||||
'algorithm': AlgorithmTypes.megolmV1AesSha2,
|
||||
'room_id': roomId,
|
||||
'forwarding_curve25519_key_chain': [client.identityKey],
|
||||
'session_id': sessionId,
|
||||
'session_key': inbound.export_session(1),
|
||||
'sender_key': senderKey,
|
||||
'sender_claimed_ed25519_key': client.fingerprintKey,
|
||||
};
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
client.encryption!.keyManager.setInboundGroupSession(
|
||||
roomId, sessionId, senderKey, sessionPayload,
|
||||
forwarded: true);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
var dbSessions = await client.database!.getInboundGroupSessionsToUpload();
|
||||
expect(dbSessions.isNotEmpty, true);
|
||||
await client.encryption!.keyManager.backgroundTasks();
|
||||
final payload = FakeMatrixApi
|
||||
.calledEndpoints['/client/unstable/room_keys/keys?version=5']!.first;
|
||||
dbSessions = await client.database!.getInboundGroupSessionsToUpload();
|
||||
expect(dbSessions.isEmpty, true);
|
||||
|
||||
final onlineKeys = RoomKeys.fromJson(json.decode(payload));
|
||||
client.encryption!.keyManager.clearInboundGroupSessions();
|
||||
var ret = client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||
expect(ret, null);
|
||||
await client.encryption!.keyManager.loadFromResponse(onlineKeys);
|
||||
ret = client.encryption!.keyManager
|
||||
.getInboundGroupSession(roomId, sessionId, senderKey);
|
||||
expect(ret != null, true);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: false);
|
||||
});
|
||||
});
|
||||
}
|
||||
501
test/encryption/ssss_test.dart
Normal file
501
test/encryption/ssss_test.dart
Normal file
|
|
@ -0,0 +1,501 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2020 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:olm/olm.dart' as olm;
|
||||
|
||||
import '../fake_client.dart';
|
||||
import '../fake_matrix_api.dart';
|
||||
|
||||
Uint8List secureRandomBytes(int len) {
|
||||
final rng = Random.secure();
|
||||
final list = Uint8List(len);
|
||||
list.setAll(0, Iterable.generate(list.length, (i) => rng.nextInt(256)));
|
||||
return list;
|
||||
}
|
||||
|
||||
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();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
await handle.maybeCacheAll();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('SSSS', () {
|
||||
Logs().level = Level.error;
|
||||
var olmEnabled = true;
|
||||
|
||||
late Client client;
|
||||
|
||||
test('setupClient', () async {
|
||||
try {
|
||||
await olm.init();
|
||||
olm.get_library_version();
|
||||
} catch (e) {
|
||||
olmEnabled = false;
|
||||
Logs().w('[LibOlm] Failed to load LibOlm', e);
|
||||
}
|
||||
Logs().i('[LibOlm] Enabled: $olmEnabled');
|
||||
if (!olmEnabled) return;
|
||||
|
||||
client = await getClient();
|
||||
});
|
||||
|
||||
test('basic things', () async {
|
||||
if (!olmEnabled) return;
|
||||
expect(client.encryption!.ssss.defaultKeyId,
|
||||
'0FajDWYaM6wQ4O60OZnLvwZfsBNu4Bu3');
|
||||
});
|
||||
|
||||
test('encrypt / decrypt', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
|
||||
final enc = await SSSS.encryptAes('secret foxies', key, 'name');
|
||||
final dec = await SSSS.decryptAes(enc, key, 'name');
|
||||
expect(dec, 'secret foxies');
|
||||
});
|
||||
|
||||
test('store', () async {
|
||||
if (!olmEnabled) return;
|
||||
final handle = client.encryption!.ssss.open();
|
||||
var failed = false;
|
||||
try {
|
||||
await handle.unlock(passphrase: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
failed = false;
|
||||
try {
|
||||
await handle.unlock(recoveryKey: 'invalid');
|
||||
} catch (_) {
|
||||
failed = true;
|
||||
}
|
||||
expect(failed, true);
|
||||
expect(handle.isUnlocked, false);
|
||||
await handle.unlock(passphrase: ssssPassphrase);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.isUnlocked, true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await handle.store('best animal', 'foxies');
|
||||
// alright, since we don't properly sync we will manually have to update
|
||||
// account_data for this test
|
||||
final content = FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/user/%40test%3AfakeServer.notExisting/account_data/best%20animal']!
|
||||
.first;
|
||||
client.accountData['best animal'] = BasicEvent.fromJson({
|
||||
'type': 'best animal',
|
||||
'content': json.decode(content),
|
||||
});
|
||||
expect(await handle.getStored('best animal'), 'foxies');
|
||||
});
|
||||
|
||||
test('encode / decode recovery key', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key = Uint8List.fromList(secureRandomBytes(32));
|
||||
final encoded = SSSS.encodeRecoveryKey(key);
|
||||
var decoded = SSSS.decodeRecoveryKey(encoded);
|
||||
expect(key, decoded);
|
||||
|
||||
decoded = SSSS.decodeRecoveryKey(encoded + ' \n\t');
|
||||
expect(key, decoded);
|
||||
|
||||
final handle = client.encryption!.ssss.open();
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(handle.recoveryKey, ssssKey);
|
||||
});
|
||||
|
||||
test('cache', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.encryption!.ssss.clearCache();
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey, postUnlock: false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
false);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
false);
|
||||
await handle.getStored(EventTypes.CrossSigningSelfSigning);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true);
|
||||
await handle.maybeCacheAll();
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('postUnlock', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningSelfSigning)) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
(await client.encryption!.ssss
|
||||
.getCached(EventTypes.CrossSigningUserSigning)) !=
|
||||
null,
|
||||
true);
|
||||
expect(
|
||||
(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup)) !=
|
||||
null,
|
||||
true);
|
||||
expect(client.userDeviceKeys[client.userID!]!.masterKey!.directVerified,
|
||||
true);
|
||||
});
|
||||
|
||||
test('make share requests', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.request('some.type', [key]);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
});
|
||||
|
||||
test('answer to share requests', () async {
|
||||
if (!olmEnabled) return;
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
true);
|
||||
|
||||
// now test some fail scenarios
|
||||
|
||||
// not by us
|
||||
event = ToDeviceEvent(
|
||||
sender: '@someotheruser:example.org',
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// secret not cached
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': 'm.unknown.secret',
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// is a cancelation
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request_cancellation',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
|
||||
// device not verified
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(false);
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!
|
||||
.setDirectVerified(false);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.request',
|
||||
content: {
|
||||
'action': 'request',
|
||||
'requesting_device_id': 'OTHERDEVICE',
|
||||
'name': EventTypes.CrossSigningSelfSigning,
|
||||
'request_id': '1',
|
||||
},
|
||||
);
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(
|
||||
FakeMatrixApi.calledEndpoints.keys.any(
|
||||
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
|
||||
false);
|
||||
key.setDirectVerified(true);
|
||||
});
|
||||
|
||||
test('receive share requests', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
final handle =
|
||||
client.encryption!.ssss.open(EventTypes.CrossSigningSelfSigning);
|
||||
await handle.unlock(recoveryKey: ssssKey);
|
||||
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
var event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), 'foxies!');
|
||||
|
||||
// test the different validators
|
||||
for (final type in [
|
||||
EventTypes.CrossSigningSelfSigning,
|
||||
EventTypes.CrossSigningUserSigning,
|
||||
EventTypes.MegolmBackup
|
||||
]) {
|
||||
final secret = await handle.getStored(type);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(type, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id':
|
||||
client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': secret,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached(type), secret);
|
||||
}
|
||||
|
||||
// test different fail scenarios
|
||||
|
||||
// not encrypted
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// unknown request id
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': 'invalid',
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// not from a device we sent the request to
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': 'invalid',
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// secret not a string
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request('best animal', [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 42,
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached('best animal'), null);
|
||||
|
||||
// validator doesn't check out
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.request(EventTypes.MegolmBackup, [key]);
|
||||
event = ToDeviceEvent(
|
||||
sender: client.userID!,
|
||||
type: 'm.secret.send',
|
||||
content: {
|
||||
'request_id': client.encryption!.ssss.pendingShareRequests.keys.first,
|
||||
'secret': 'foxies!',
|
||||
},
|
||||
encryptedContent: {
|
||||
'sender_key': key.curve25519Key,
|
||||
},
|
||||
);
|
||||
await client.encryption!.ssss.handleToDeviceEvent(event);
|
||||
expect(await client.encryption!.ssss.getCached(EventTypes.MegolmBackup),
|
||||
null);
|
||||
});
|
||||
|
||||
test('request all', () async {
|
||||
if (!olmEnabled) return;
|
||||
final key =
|
||||
client.userDeviceKeys[client.userID!]!.deviceKeys['OTHERDEVICE']!;
|
||||
key.setDirectVerified(true);
|
||||
await client.encryption!.ssss.clearCache();
|
||||
client.encryption!.ssss.pendingShareRequests.clear();
|
||||
await client.encryption!.ssss.maybeRequestAll([key]);
|
||||
expect(client.encryption!.ssss.pendingShareRequests.length, 3);
|
||||
});
|
||||
|
||||
test('periodicallyRequestMissingCache', () async {
|
||||
if (!olmEnabled) return;
|
||||
client.userDeviceKeys[client.userID!]!.masterKey!.setDirectVerified(true);
|
||||
client.encryption!.ssss = MockSSSS(client.encryption!);
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, true);
|
||||
// it should only retry once every 15 min
|
||||
(client.encryption!.ssss as MockSSSS).requestedSecrets = false;
|
||||
await client.encryption!.ssss.periodicallyRequestMissingCache();
|
||||
expect((client.encryption!.ssss as MockSSSS).requestedSecrets, false);
|
||||
});
|
||||
|
||||
test('createKey', () async {
|
||||
if (!olmEnabled) return;
|
||||
// with passphrase
|
||||
var newKey = await client.encryption!.ssss.createKey('test');
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
var testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.unlock(passphrase: 'test');
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
|
||||
// without passphrase
|
||||
newKey = await client.encryption!.ssss.createKey();
|
||||
expect(client.encryption!.ssss.isKeyValid(newKey.keyId), true);
|
||||
testKey = client.encryption!.ssss.open(newKey.keyId);
|
||||
await testKey.setPrivateKey(newKey.privateKey!);
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
if (!olmEnabled) return;
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
76
test/encryption/utils_test.dart
Normal file
76
test/encryption/utils_test.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2022 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:matrix/encryption/utils/base64_unpadded.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Utils', () {
|
||||
const base64input = 'foobar';
|
||||
final utf8codec = Utf8Codec();
|
||||
test('base64 padded', () {
|
||||
final paddedBase64 = base64.encode(base64input.codeUnits);
|
||||
|
||||
final decodedPadded =
|
||||
utf8codec.decode(base64decodeUnpadded(paddedBase64));
|
||||
expect(decodedPadded, base64input, reason: 'Padded base64 decode');
|
||||
});
|
||||
|
||||
test('base64 unpadded', () {
|
||||
const unpaddedBase64 = 'Zm9vYmFy';
|
||||
final decodedUnpadded =
|
||||
utf8codec.decode(base64decodeUnpadded(unpaddedBase64));
|
||||
expect(decodedUnpadded, base64input, reason: 'Unpadded base64 decode');
|
||||
});
|
||||
});
|
||||
|
||||
group('MatrixFile', () {
|
||||
test('MatrixImageFile', () async {
|
||||
const base64Image =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC';
|
||||
final data = base64Decode(base64Image);
|
||||
|
||||
final image = await MatrixImageFile.create(
|
||||
bytes: data,
|
||||
name: 'bomb.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
expect(image.width, 220, reason: 'Unexpected image width');
|
||||
expect(image.height, 220, reason: 'Unexpected image heigth');
|
||||
expect(image.blurhash, 'L75NyU5krSbx=zAF#kSNZxOZ%4NE',
|
||||
reason: 'Unexpected image blur');
|
||||
|
||||
final thumbnail = await image.generateThumbnail(dimension: 64);
|
||||
expect(thumbnail!.height, 64, reason: 'Unexpected thumbnail height');
|
||||
|
||||
final shrinkedImage = await MatrixImageFile.shrink(
|
||||
bytes: data,
|
||||
name: 'bomb.png',
|
||||
mimeType: 'image/png',
|
||||
maxDimension: 150);
|
||||
expect(shrinkedImage.width, 150, reason: 'Unexpected scaled image width');
|
||||
expect(shrinkedImage.height, 150,
|
||||
reason: 'Unexpected scaled image heigth');
|
||||
expect(shrinkedImage.blurhash, 'L75NyU5kvvbx^7AF#kSgZxOZ%5NE',
|
||||
reason: 'Unexpected scaled image blur');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue