configurable verification methods
This commit is contained in:
parent
d7f2bbe2f9
commit
9971e7377e
|
@ -90,16 +90,25 @@ class Client {
|
|||
SSSS ssss;
|
||||
CrossSigning crossSigning;
|
||||
|
||||
Set<KeyVerificationMethod> verificationMethods;
|
||||
|
||||
/// Create a client
|
||||
/// clientName = unique identifier of this client
|
||||
/// debug: Print debug output?
|
||||
/// database: The database instance to use
|
||||
/// 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,
|
||||
{this.debug = false, this.database, this.enableE2eeRecovery = false}) {
|
||||
{this.debug = false,
|
||||
this.database,
|
||||
this.enableE2eeRecovery = false,
|
||||
this.verificationMethods}) {
|
||||
ssss = SSSS(this);
|
||||
keyManager = KeyManager(this);
|
||||
crossSigning = CrossSigning(this);
|
||||
verificationMethods ??= {};
|
||||
onLoginStateChanged.stream.listen((loginState) {
|
||||
print('LoginState: ${loginState.toString()}');
|
||||
});
|
||||
|
@ -1209,7 +1218,8 @@ class Client {
|
|||
}
|
||||
|
||||
void _handleToDeviceKeyVerificationRequest(ToDeviceEvent toDeviceEvent) {
|
||||
if (!toDeviceEvent.type.startsWith('m.key.verification.')) {
|
||||
if (!toDeviceEvent.type.startsWith('m.key.verification.') ||
|
||||
verificationMethods.isEmpty) {
|
||||
return;
|
||||
}
|
||||
// we have key verification going on!
|
||||
|
@ -1243,7 +1253,8 @@ class Client {
|
|||
final type = event['type'].startsWith('m.key.verification.')
|
||||
? event['type']
|
||||
: event['content']['msgtype'];
|
||||
if (!type.startsWith('m.key.verification.')) {
|
||||
if (!type.startsWith('m.key.verification.') ||
|
||||
verificationMethods.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (type == 'm.key.verification.request') {
|
||||
|
|
|
@ -16,7 +16,8 @@ class CrossSigning {
|
|||
client.ssss.setValidator(SELF_SIGNING_KEY, (String secret) async {
|
||||
final keyObj = olm.PkSigning();
|
||||
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 (_) {
|
||||
return false;
|
||||
} finally {
|
||||
|
@ -26,7 +27,8 @@ class CrossSigning {
|
|||
client.ssss.setValidator(USER_SIGNING_KEY, (String secret) async {
|
||||
final keyObj = olm.PkSigning();
|
||||
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 (_) {
|
||||
return false;
|
||||
} finally {
|
||||
|
|
|
@ -20,7 +20,8 @@ class KeyManager {
|
|||
final keyObj = olm.PkDecryption();
|
||||
try {
|
||||
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 (_) {
|
||||
return false;
|
||||
} finally {
|
||||
|
@ -59,13 +60,16 @@ class KeyManager {
|
|||
try {
|
||||
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;
|
||||
}
|
||||
for (final roomEntries in payload['rooms'].entries) {
|
||||
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;
|
||||
}
|
||||
for (final sessionEntries in roomEntries.value['sessions'].entries) {
|
||||
|
@ -74,24 +78,35 @@ class KeyManager {
|
|||
if (!(rawEncryptedSession is Map)) {
|
||||
continue;
|
||||
}
|
||||
final firstMessageIndex = rawEncryptedSession['first_message_index'] is int ? 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 firstMessageIndex =
|
||||
rawEncryptedSession['first_message_index'] is int
|
||||
? 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'];
|
||||
if (firstMessageIndex == null || forwardedCount == null || isVerified == null || !(sessionData is Map)) {
|
||||
if (firstMessageIndex == null ||
|
||||
forwardedCount == null ||
|
||||
isVerified == null ||
|
||||
!(sessionData is Map)) {
|
||||
continue;
|
||||
}
|
||||
final senderKey = sessionData['sender_key'];
|
||||
Map<String, dynamic> decrypted;
|
||||
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) {
|
||||
print('[LibOlm] Error decrypting room key: ' + err.toString());
|
||||
}
|
||||
if (decrypted != null) {
|
||||
decrypted['session_id'] = sessionId;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +120,8 @@ class KeyManager {
|
|||
final info = await getRoomKeysInfo();
|
||||
final ret = await client.jsonRequest(
|
||||
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({
|
||||
'rooms': {
|
||||
|
|
|
@ -72,9 +72,7 @@ class SSSS {
|
|||
final keys = deriveKeys(key, name);
|
||||
final cipher = base64.decode(data.ciphertext);
|
||||
final hmac = base64
|
||||
.encode(Hmac(sha256, keys.hmacKey)
|
||||
.convert(cipher)
|
||||
.bytes)
|
||||
.encode(Hmac(sha256, keys.hmacKey).convert(cipher).bytes)
|
||||
.replaceAll(RegExp(r'=+$'), '');
|
||||
if (hmac != data.mac.replaceAll(RegExp(r'=+$'), '')) {
|
||||
throw 'Bad MAC';
|
||||
|
@ -322,7 +320,8 @@ class SSSS {
|
|||
return; // the secret wasn't a string....wut?
|
||||
}
|
||||
// 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');
|
||||
return; // didn't pass the validator
|
||||
}
|
||||
|
@ -335,8 +334,8 @@ class SSSS {
|
|||
if (client.database != null) {
|
||||
final keyId = keyIdFromType(request.type);
|
||||
if (keyId != null) {
|
||||
await client.database.storeSSSSCache(
|
||||
client.id, request.type, keyId, secret);
|
||||
await client.database
|
||||
.storeSSSSCache(client.id, request.type, keyId, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ class DeviceKeysList {
|
|||
return null;
|
||||
}
|
||||
|
||||
CrossSigningKey getCrossSigningKey(String type) =>
|
||||
crossSigningKeys.values.firstWhere((k) => k.usage.contains(type), orElse: () => null);
|
||||
CrossSigningKey getCrossSigningKey(String type) => crossSigningKeys.values
|
||||
.firstWhere((k) => k.usage.contains(type), orElse: () => null);
|
||||
|
||||
CrossSigningKey get masterKey => getCrossSigningKey('master');
|
||||
CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing');
|
||||
|
|
|
@ -52,6 +52,8 @@ enum KeyVerificationState {
|
|||
error
|
||||
}
|
||||
|
||||
enum KeyVerificationMethod { emoji, numbers }
|
||||
|
||||
List<String> _intersect(List<String> a, List<dynamic> b) {
|
||||
final res = <String>[];
|
||||
for (final v in a) {
|
||||
|
@ -82,8 +84,6 @@ List<int> _bytesToInt(Uint8List bytes, int totalBits) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
final VERIFICATION_METHODS = ['m.sas.v1'];
|
||||
|
||||
_KeyVerificationMethod _makeVerificationMethod(
|
||||
String type, KeyVerification request) {
|
||||
if (type == 'm.sas.v1') {
|
||||
|
@ -134,9 +134,18 @@ class KeyVerification {
|
|||
: 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 {
|
||||
await send('m.key.verification.request', {
|
||||
'methods': VERIFICATION_METHODS,
|
||||
'methods': knownVerificationMethods,
|
||||
if (room == null) 'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
startedVerification = true;
|
||||
|
@ -178,7 +187,7 @@ class KeyVerification {
|
|||
}
|
||||
// verify it has a method we can use
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
_intersect(knownVerificationMethods, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
|
@ -189,7 +198,7 @@ class KeyVerification {
|
|||
case 'm.key.verification.ready':
|
||||
_deviceId ??= payload['from_device'];
|
||||
possibleMethods =
|
||||
_intersect(VERIFICATION_METHODS, payload['methods']);
|
||||
_intersect(knownVerificationMethods, payload['methods']);
|
||||
if (possibleMethods.isEmpty) {
|
||||
// reject it outright
|
||||
await cancel('m.unknown_method');
|
||||
|
@ -233,7 +242,7 @@ class KeyVerification {
|
|||
if (!(await verifyLastStep(['m.key.verification.request', null]))) {
|
||||
return; // abort
|
||||
}
|
||||
if (!VERIFICATION_METHODS.contains(payload['method'])) {
|
||||
if (!knownVerificationMethods.contains(payload['method'])) {
|
||||
await cancel('m.unknown_method');
|
||||
return;
|
||||
}
|
||||
|
@ -525,7 +534,6 @@ abstract class _KeyVerificationMethod {
|
|||
const KNOWN_KEY_AGREEMENT_PROTOCOLS = ['curve25519-hkdf-sha256', 'curve25519'];
|
||||
const KNOWN_HASHES = ['sha256'];
|
||||
const KNOWN_MESSAGE_AUTHENTIFICATION_CODES = ['hkdf-hmac-sha256'];
|
||||
const KNOWN_AUTHENTICATION_TYPES = ['emoji', 'decimal'];
|
||||
|
||||
class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
||||
_KeyVerificationMethodSas({KeyVerification request})
|
||||
|
@ -549,6 +557,19 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
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
|
||||
Future<void> handlePayload(String type, Map<String, dynamic> payload) async {
|
||||
try {
|
||||
|
@ -630,7 +651,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
'key_agreement_protocols': KNOWN_KEY_AGREEMENT_PROTOCOLS,
|
||||
'hashes': KNOWN_HASHES,
|
||||
'message_authentication_codes': KNOWN_MESSAGE_AUTHENTIFICATION_CODES,
|
||||
'short_authentication_string': KNOWN_AUTHENTICATION_TYPES,
|
||||
'short_authentication_string': knownAuthentificationTypes,
|
||||
};
|
||||
request.makePayload(payload);
|
||||
// We just store the canonical json in here for later verification
|
||||
|
@ -662,7 +683,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
}
|
||||
messageAuthenticationCode = possibleMessageAuthenticationCodes.first;
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
@ -700,7 +721,7 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
|
|||
}
|
||||
messageAuthenticationCode = payload['message_authentication_code'];
|
||||
final possibleAuthenticationTypes = _intersect(
|
||||
KNOWN_AUTHENTICATION_TYPES, payload['short_authentication_string']);
|
||||
knownAuthentificationTypes, payload['short_authentication_string']);
|
||||
if (possibleAuthenticationTypes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -410,11 +410,9 @@ packages:
|
|||
olm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "0c612a525511652a7760126b058de8c924fe8900"
|
||||
resolved-ref: "0c612a525511652a7760126b058de8c924fe8900"
|
||||
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
||||
source: git
|
||||
path: "/home/sorunome/repos/famedly/dart-olm"
|
||||
relative: false
|
||||
source: path
|
||||
version: "1.1.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
|
|
Loading…
Reference in a new issue