configurable verification methods

This commit is contained in:
Sorunome 2020-05-30 13:55:09 +02:00
parent d7f2bbe2f9
commit 9971e7377e
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
7 changed files with 87 additions and 40 deletions

View File

@ -90,16 +90,25 @@ class Client {
SSSS ssss; SSSS ssss;
CrossSigning crossSigning; CrossSigning crossSigning;
Set<KeyVerificationMethod> verificationMethods;
/// Create a client /// Create a client
/// clientName = unique identifier of this client /// clientName = unique identifier of this client
/// debug: Print debug output? /// debug: Print debug output?
/// database: The database instance to use /// database: The database instance to use
/// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions /// enableE2eeRecovery: Enable additional logic to try to recover from bad e2ee sessions
/// verificationMethods: A set of all the verification methods this client can handle. Includes:
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
/// KeyVerificationMethod.emoji: Compare emojis
Client(this.clientName, Client(this.clientName,
{this.debug = false, this.database, this.enableE2eeRecovery = false}) { {this.debug = false,
this.database,
this.enableE2eeRecovery = false,
this.verificationMethods}) {
ssss = SSSS(this); ssss = SSSS(this);
keyManager = KeyManager(this); keyManager = KeyManager(this);
crossSigning = CrossSigning(this); crossSigning = CrossSigning(this);
verificationMethods ??= {};
onLoginStateChanged.stream.listen((loginState) { onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}'); print('LoginState: ${loginState.toString()}');
}); });
@ -1209,7 +1218,8 @@ class Client {
} }
void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) { void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) {
if (!toDeviceEvent.type.startsWith('m.key.verification.')) { if (!toDeviceEvent.type.startsWith('m.key.verification.') ||
verificationMethods.isEmpty) {
return; return;
} }
// we have key verification going on! // we have key verification going on!
@ -1243,7 +1253,8 @@ class Client {
final type = event['type'].startsWith('m.key.verification.') final type = event['type'].startsWith('m.key.verification.')
? event['type'] ? event['type']
: event['content']['msgtype']; : event['content']['msgtype'];
if (!type.startsWith('m.key.verification.')) { if (!type.startsWith('m.key.verification.') ||
verificationMethods.isEmpty) {
return; return;
} }
if (type == 'm.key.verification.request') { if (type == 'm.key.verification.request') {

View File

@ -16,7 +16,8 @@ class CrossSigning {
client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async { client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
final keyObj = olm.PkSigning(); final keyObj = olm.PkSigning();
try { try {
return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key; return keyObj.init_with_seed(base64.decode(secret)) ==
client.userDeviceKeys[client.userID].selfSigningKey.ed25519Key;
} catch (_) { } catch (_) {
return false; return false;
} finally { } finally {
@ -26,7 +27,8 @@ class CrossSigning {
client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async { client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
final keyObj = olm.PkSigning(); final keyObj = olm.PkSigning();
try { try {
return keyObj.init_with_seed(base64.decode(secret)) == client.userDeviceKeys[client.userID].userSigningKey.ed25519Key; return keyObj.init_with_seed(base64.decode(secret)) ==
client.userDeviceKeys[client.userID].userSigningKey.ed25519Key;
} catch (_) { } catch (_) {
return false; return false;
} finally { } finally {

View File

@ -20,7 +20,8 @@ class KeyManager {
final keyObj = olm.PkDecryption(); final keyObj = olm.PkDecryption();
try { try {
final info = await getRoomKeysInfo(); final info = await getRoomKeysInfo();
return keyObj.init_with_private_key(base64.decode(secret)) == info['auth_data']['public_key']; return keyObj.init_with_private_key(base64.decode(secret)) ==
info['auth_data']['public_key'];
} catch (_) { } catch (_) {
return false; return false;
} finally { } finally {
@ -59,13 +60,16 @@ class KeyManager {
try { try {
backupPubKey = decryption.init_with_private_key(privateKey); backupPubKey = decryption.init_with_private_key(privateKey);
if (backupPubKey == null || !info.containsKey('auth_data') || !(info['auth_data'] is Map) || info['auth_data']['public_key'] != backupPubKey) { if (backupPubKey == null ||
!info.containsKey('auth_data') ||
!(info['auth_data'] is Map) ||
info['auth_data']['public_key'] != backupPubKey) {
return; return;
} }
for (final roomEntries in payload['rooms'].entries) { for (final roomEntries in payload['rooms'].entries) {
final roomId = roomEntries.key; final roomId = roomEntries.key;
if (!(roomEntries.value is Map) || !(roomEntries.value['sessions'] is Map)) { if (!(roomEntries.value is Map) ||
!(roomEntries.value['sessions'] is Map)) {
continue; continue;
} }
for (final sessionEntries in roomEntries.value['sessions'].entries) { for (final sessionEntries in roomEntries.value['sessions'].entries) {
@ -74,24 +78,35 @@ class KeyManager {
if (!(rawEncryptedSession is Map)) { if (!(rawEncryptedSession is Map)) {
continue; continue;
} }
final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? rawEncryptedSession['first_message_index'] : null; final firstMessageIndex =
final forwardedCount = rawEncryptedSession['forwarded_count'] is int ? rawEncryptedSession['forwarded_count'] : null; rawEncryptedSession['first_message_index'] is int
final isVerified = rawEncryptedSession['is_verified'] is bool ? rawEncryptedSession['is_verified'] : null; ? rawEncryptedSession['first_message_index']
: null;
final forwardedCount = rawEncryptedSession['forwarded_count'] is int
? rawEncryptedSession['forwarded_count']
: null;
final isVerified = rawEncryptedSession['is_verified'] is bool
? rawEncryptedSession['is_verified']
: null;
final sessionData = rawEncryptedSession['session_data']; final sessionData = rawEncryptedSession['session_data'];
if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) { if (firstMessageIndex == null ||
forwardedCount == null ||
isVerified == null ||
!(sessionData is Map)) {
continue; continue;
} }
final senderKey = sessionData['sender_key'];
Map<String, dynamic> decrypted; Map<String, dynamic> decrypted;
try { try {
decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'], sessionData['mac'], sessionData['ciphertext'])); decrypted = json.decode(decryption.decrypt(sessionData['ephemeral'],
sessionData['mac'], sessionData['ciphertext']));
} catch (err) { } catch (err) {
print('[LibOlm] Error decrypting room key: ' + err.toString()); print('[LibOlm] Error decrypting room key: ' + err.toString());
} }
if (decrypted != null) { if (decrypted != null) {
decrypted['session_id'] = sessionId; decrypted['session_id'] = sessionId;
decrypted['room_id'] = roomId; decrypted['room_id'] = roomId;
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client); final room =
client.getRoomById(roomId) ?? Room(id: roomId, client: client);
room.setInboundGroupSession(sessionId, decrypted, forwarded: true); room.setInboundGroupSession(sessionId, decrypted, forwarded: true);
} }
} }
@ -105,7 +120,8 @@ class KeyManager {
final info = await getRoomKeysInfo(); final info = await getRoomKeysInfo();
final ret = await client.jsonRequest( final ret = await client.jsonRequest(
type: HTTPType.GET, type: HTTPType.GET,
action: '/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}', action:
'/client/r0/room_keys/keys/${Uri.encodeComponent(roomId)}/${Uri.encodeComponent(sessionId)}?version=${info['version']}',
); );
await loadFromResponse({ await loadFromResponse({
'rooms': { 'rooms': {

View File

@ -72,9 +72,7 @@ class SSSS {
final keys = deriveKeys(key, name); final keys = deriveKeys(key, name);
final cipher = base64.decode(data.ciphertext); final cipher = base64.decode(data.ciphertext);
final hmac = base64 final hmac = base64
.encode(Hmac(sha256, keys.hmacKey) .encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
.convert(cipher)
.bytes)
.replaceAll(RegExp(r'=+$'), ''); .replaceAll(RegExp(r'=+$'), '');
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) { if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
throw 'Bad MAC'; throw 'Bad MAC';
@ -322,7 +320,8 @@ class SSSS {
return; // the secret wasn't a string....wut? return; // the secret wasn't a string....wut?
} }
// let's validate if the secret is, well, valid // let's validate if the secret is, well, valid
if (_validators.containsKey(request.type) && !(await _validators[request.type](secret))) { if (_validators.containsKey(request.type) &&
!(await _validators[request.type](secret))) {
print('[SSSS] The received secret was invalid'); print('[SSSS] The received secret was invalid');
return; // didn't pass the validator return; // didn't pass the validator
} }
@ -335,8 +334,8 @@ class SSSS {
if (client.database != null) { if (client.database != null) {
final keyId = keyIdFromType(request.type); final keyId = keyIdFromType(request.type);
if (keyId != null) { if (keyId != null) {
await client.database.storeSSSSCache( await client.database
client.id, request.type, keyId, secret); .storeSSSSCache(client.id, request.type, keyId, secret);
} }
} }
} }

View File

@ -29,8 +29,8 @@ class DeviceKeysList {
return null; return null;
} }
CrossSigningKey getCrossSigningKey(String type) => CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values
crossSigningKeys.values.firstWhere((k) => k.usage.contains(type), orElse: () => null); .firstWhere((k) => k.usage.contains(type), orElse: () => null);
CrossSigningKey get masterKey => getCrossSigningKey('master'); CrossSigningKey get masterKey => getCrossSigningKey('master');
CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing'); CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing');

View File

@ -52,6 +52,8 @@ enum KeyVerificationState {
error error
} }
enum KeyVerificationMethod { emoji, numbers }
List<String> _intersect(List<String> a, List<dynamic> b) { List<String> _intersect(List<String> a, List<dynamic> b) {
final res = <String>[]; final res = <String>[];
for (final v in a) { for (final v in a) {
@ -82,8 +84,6 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
return ret; return ret;
} }
final VERIFICATION_METHODS = ['m.sas.v1'];
_KeyVerificationMethod _makeVerificationMethod( _KeyVerificationMethod _makeVerificationMethod(
String type, KeyVerification request) { String type, KeyVerification request) {
if (type == 'm.sas.v1') { if (type == 'm.sas.v1') {
@ -134,9 +134,18 @@ class KeyVerification {
: null); : null);
} }
List<String> get knownVerificationMethods {
final methods = <String>[];
if (client.verificationMethods.contains(KeyVerificationMethod.numbers) ||
client.verificationMethods.contains(KeyVerificationMethod.emoji)) {
methods.add('m.sas.v1');
}
return methods;
}
Future<void> sendStart() async { Future<void> sendStart() async {
await send('m.key.verification.request', { await send('m.key.verification.request', {
'methods': VERIFICATION_METHODS, 'methods': knownVerificationMethods,
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch, if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
}); });
startedVerification = true; startedVerification = true;
@ -178,7 +187,7 @@ class KeyVerification {
} }
// verify it has a method we can use // verify it has a method we can use
possibleMethods = possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']); _intersect(knownVerificationMethods, payload['methods']);
if (possibleMethods.isEmpty) { if (possibleMethods.isEmpty) {
// reject it outright // reject it outright
await cancel('m.unknown_method'); await cancel('m.unknown_method');
@ -189,7 +198,7 @@ class KeyVerification {
case 'm.key.verification.ready': case 'm.key.verification.ready':
_deviceId ??= payload['from_device']; _deviceId ??= payload['from_device'];
possibleMethods = possibleMethods =
_intersect(VERIFICATION_METHODS, payload['methods']); _intersect(knownVerificationMethods, payload['methods']);
if (possibleMethods.isEmpty) { if (possibleMethods.isEmpty) {
// reject it outright // reject it outright
await cancel('m.unknown_method'); await cancel('m.unknown_method');
@ -233,7 +242,7 @@ class KeyVerification {
if (!(await verifyLastStep(['m.key.verification.request', null]))) { if (!(await verifyLastStep(['m.key.verification.request', null]))) {
return; // abort return; // abort
} }
if (!VERIFICATION_METHODS.contains(payload['method'])) { if (!knownVerificationMethods.contains(payload['method'])) {
await cancel('m.unknown_method'); await cancel('m.unknown_method');
return; return;
} }
@ -525,7 +534,6 @@ abstract class _KeyVerificationMethod {
const KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519']; const KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519'];
const KNOWN_HASHES = ['sha256']; const KNOWN_HASHES = ['sha256'];
const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256']; const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
class _KeyVerificationMethodSas extends _KeyVerificationMethod { class _KeyVerificationMethodSas extends _KeyVerificationMethod {
_KeyVerificationMethodSas({KeyVerification request}) _KeyVerificationMethodSas({KeyVerification request})
@ -549,6 +557,19 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
sas?.free(); sas?.free();
} }
List<String> get knownAuthentificationTypes {
final types = <String>[];
if (request.client.verificationMethods
.contains(KeyVerificationMethod.emoji)) {
types.add('emoji');
}
if (request.client.verificationMethods
.contains(KeyVerificationMethod.numbers)) {
types.add('decimal');
}
return types;
}
@override @override
Future<void> handlePayload(String type, Map<String, dynamic> payload) async { Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
try { try {
@ -630,7 +651,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS, 'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS,
'hashes': KNOWN_HASHES, 'hashes': KNOWN_HASHES,
'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES, 'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
'short_authentication_string': KNOWN_AUTHENTICATION_TYPES, 'short_authentication_string': knownAuthentificationTypes,
}; };
request.makePayload(payload); request.makePayload(payload);
// We just store the canonical json in here for later verification // We just store the canonical json in here for later verification
@ -662,7 +683,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
messageAuthenticationCode = possibleMessageAuthenticationCodes.first; messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
final possibleAuthenticationTypes = _intersect( final possibleAuthenticationTypes = _intersect(
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); knownAuthentificationTypes, payload['short_authentication_string']);
if (possibleAuthenticationTypes.isEmpty) { if (possibleAuthenticationTypes.isEmpty) {
return false; return false;
} }
@ -700,7 +721,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
} }
messageAuthenticationCode = payload['message_authentication_code']; messageAuthenticationCode = payload['message_authentication_code'];
final possibleAuthenticationTypes = _intersect( final possibleAuthenticationTypes = _intersect(
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']); knownAuthentificationTypes, payload['short_authentication_string']);
if (possibleAuthenticationTypes.isEmpty) { if (possibleAuthenticationTypes.isEmpty) {
return false; return false;
} }

View File

@ -410,11 +410,9 @@ packages:
olm: olm:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "/home/sorunome/repos/famedly/dart-olm"
ref: "0c612a525511652a7760126b058de8c924fe8900" relative: false
resolved-ref: "0c612a525511652a7760126b058de8c924fe8900" source: path
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
source: git
version: "1.1.1" version: "1.1.1"
package_config: package_config:
dependency: transitive dependency: transitive