configurable verification methods
This commit is contained in:
parent
d7f2bbe2f9
commit
9971e7377e
|
@ -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') {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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': {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue