Initial commit version 0.8.13

This commit is contained in:
PCoder 2022-04-18 14:27:08 +05:30
commit 9526dfa4f2
111 changed files with 35074 additions and 0 deletions

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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);
});
});
}

View 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');
});
});
}