in theory sign others keys

This commit is contained in:
Sorunome 2020-05-25 15:30:53 +02:00
parent 97a10c7de1
commit c13f66c85f
No known key found for this signature in database
GPG key ID: B19471D07FC9BE9C
5 changed files with 261 additions and 67 deletions

View file

@ -48,6 +48,7 @@ import 'package:pedantic/pedantic.dart';
import 'event.dart'; import 'event.dart';
import 'room.dart'; import 'room.dart';
import 'ssss.dart'; import 'ssss.dart';
import 'cross_signing.dart';
import 'sync/event_update.dart'; import 'sync/event_update.dart';
import 'sync/room_update.dart'; import 'sync/room_update.dart';
import 'sync/user_update.dart'; import 'sync/user_update.dart';
@ -81,6 +82,7 @@ class Client {
bool enableE2eeRecovery; bool enableE2eeRecovery;
SSSS ssss; SSSS ssss;
CrossSigning crossSigning;
/// Create a client /// Create a client
/// clientName = unique identifier of this client /// clientName = unique identifier of this client
@ -90,6 +92,7 @@ class Client {
Client(this.clientName, Client(this.clientName,
{this.debug = false, this.database, this.enableE2eeRecovery = false}) { {this.debug = false, this.database, this.enableE2eeRecovery = false}) {
ssss = SSSS(this); ssss = SSSS(this);
crossSigning = CrossSigning(this);
onLoginStateChanged.stream.listen((loginState) { onLoginStateChanged.stream.listen((loginState) {
print('LoginState: ${loginState.toString()}'); print('LoginState: ${loginState.toString()}');
}); });
@ -1845,6 +1848,11 @@ class Client {
return payload; return payload;
} }
/// Just gets the signature of a string
String signString(String s) {
return _olmAccount.sign(s);
}
/// Checks the signature of a signed json object. /// Checks the signature of a signed json object.
bool checkJsonSignature(String key, Map<String, dynamic> signedJson, bool checkJsonSignature(String key, Map<String, dynamic> signedJson,
String userId, String deviceId) { String userId, String deviceId) {

126
lib/src/cross_signing.dart Normal file
View file

@ -0,0 +1,126 @@
import 'dart:typed_data';
import 'dart:convert';
import 'package:olm/olm.dart' as olm;
import 'client.dart';
import 'utils/device_keys_list.dart';
const SELF_SIGNING_KEY = 'm.cross_signing.self_signing';
const USER_SIGNING_KEY = 'm.cross_signing.user_signing';
const MASTER_KEY = 'm.cross_signing.master';
class CrossSigning {
final Client client;
CrossSigning(this.client);
bool get enabled =>
client.accountData[SELF_SIGNING_KEY] != null &&
client.accountData[USER_SIGNING_KEY] != null &&
client.accountData[MASTER_KEY] != null;
Future<bool> isCached() async {
if (!enabled) {
return false;
}
return (await client.ssss.getCached(SELF_SIGNING_KEY)) != null &&
(await client.ssss.getCached(USER_SIGNING_KEY)) != null;
}
bool signable(List<SignedKey> keys) {
for (final key in keys) {
if (key is CrossSigningKey && key.usage.contains('master')) {
return true;
}
if (key.userId == client.userID &&
(key is DeviceKeys) &&
key.identifier != client.deviceID) {
return true;
}
}
return false;
}
Future<void> sign(List<SignedKey> keys) async {
Uint8List selfSigningKey;
Uint8List userSigningKey;
final signatures = <String, dynamic>{};
var signedKey = false;
final addSignature =
(SignedKey key, SignedKey signedWith, String signature) {
if (key == null || signedWith == null || signature == null) {
return;
}
if (!signatures.containsKey(key.userId)) {
signatures[key.userId] = <String, dynamic>{};
}
if (!signatures[key.userId].containsKey(key.identifier)) {
signatures[key.userId][key.identifier] = key.toJson();
}
if (!signatures[key.userId][key.identifier].containsKey('signatures')) {
signatures[key.userId][key.identifier]
['signatures'] = <String, dynamic>{};
}
if (!signatures[key.userId][key.identifier]['signatures']
.containsKey(signedWith.userId)) {
signatures[key.userId][key.identifier]['signatures']
[signedWith.userId] = <String, dynamic>{};
}
signatures[key.userId][key.identifier]['signatures'][signedWith.userId]
[signedWith.identifier] = signature;
signedKey = true;
};
for (final key in keys) {
if (key.userId == client.userID) {
// we are singing a key of ourself
if (key is CrossSigningKey) {
if (key.usage.contains('master')) {
// okay, we'll sign our own master key
final signature = client.signString(key.signingContent);
addSignature(
key,
client.userDeviceKeys[client.userID].deviceKeys[client.deviceID],
signature);
}
// we don't care about signing other cross-signing keys
} else if (key.identifier != client.deviceID) {
// okay, we'll sign a device key with our self signing key
selfSigningKey ??=
base64.decode(await client.ssss.getCached(SELF_SIGNING_KEY) ?? '');
if (selfSigningKey != null) {
final signature = _sign(key.signingContent, selfSigningKey);
addSignature(key, client.userDeviceKeys[client.userID].selfSigningKey,
signature);
}
}
} else if (key is CrossSigningKey && key.usage.contains('master')) {
// we are signing someone elses master key
userSigningKey ??=
base64.decode(await client.ssss.getCached(USER_SIGNING_KEY) ?? '');
if (userSigningKey != null) {
final signature = _sign(key.signingContent, userSigningKey);
addSignature(
key, client.userDeviceKeys[client.userID].userSigningKey, signature);
}
}
}
if (signedKey) {
// post our new keys!
await client.jsonRequest(
type: HTTPType.POST,
action: '/client/r0/keys/signatures/upload',
data: signatures,
);
}
}
String _sign(String canonicalJson, Uint8List key) {
final keyObj = olm.PkSigning();
keyObj.init_with_seed(key);
try {
return keyObj.sign(canonicalJson);
} finally {
keyObj.free();
}
}
}

View file

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import "package:base58check/base58.dart"; import 'package:base58check/base58.dart';
import 'package:password_hash/password_hash.dart'; import 'package:password_hash/password_hash.dart';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
@ -21,7 +21,7 @@ const ZERO_STR =
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'; '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
const BASE58_ALPHABET = const BASE58_ALPHABET =
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const base58 = const Base58Codec(BASE58_ALPHABET); const base58 = Base58Codec(BASE58_ALPHABET);
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm const OLM_PRIVATE_KEY_LENGTH = 32; // TODO: fetch from dart-olm
const AES_BLOCKSIZE = 16; const AES_BLOCKSIZE = 16;
@ -38,7 +38,6 @@ class SSSS {
b[0] = 1; b[0] = 1;
final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b); final aesKey = Hmac(sha256, prk.bytes).convert(utf8.encode(name) + b);
b[0] = 2; b[0] = 2;
final a = aesKey.bytes + utf8.encode(name) + b;
final hmacKey = final hmacKey =
Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b); Hmac(sha256, prk.bytes).convert(aesKey.bytes + utf8.encode(name) + b);
return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes); return _DerivedKeys(aesKey: aesKey.bytes, hmacKey: hmacKey.bytes);
@ -239,6 +238,15 @@ class SSSS {
} }
} }
Future<void> maybeCacheAll(String keyId, Uint8List key) async {
for (final type in CACHE_TYPES) {
final secret = await getCached(type);
if (secret == null) {
await getStored(type, keyId, key);
}
}
}
Future<void> maybeRequestAll(List<DeviceKeys> devices) async { Future<void> maybeRequestAll(List<DeviceKeys> devices) async {
for (final type in CACHE_TYPES) { for (final type in CACHE_TYPES) {
final secret = await getCached(type); final secret = await getCached(type);
@ -347,8 +355,7 @@ class SSSS {
return null; return null;
} }
if (data.content['encrypted'] is Map) { if (data.content['encrypted'] is Map) {
final keys = Set<String>(); final Set keys = <String>{};
String maybeKey;
for (final key in data.content['encrypted'].keys) { for (final key in data.content['encrypted'].keys) {
keys.add(key); keys.add(key);
} }
@ -369,9 +376,7 @@ class SSSS {
} }
OpenSSSS open([String identifier]) { OpenSSSS open([String identifier]) {
if (identifier == null) { identifier ??= defaultKeyId;
identifier = defaultKeyId;
}
if (identifier == null) { if (identifier == null) {
throw 'Dont know what to open'; throw 'Dont know what to open';
} }
@ -454,4 +459,8 @@ class OpenSSSS {
Future<String> getStored(String type) async { Future<String> getStored(String type) async {
return await ssss.getStored(type, keyId, privateKey); return await ssss.getStored(type, keyId, privateKey);
} }
Future<void> maybeCacheAll() async {
await ssss.maybeCacheAll(keyId, privateKey);
}
} }

View file

@ -15,6 +15,28 @@ class DeviceKeysList {
Map<String, DeviceKeys> deviceKeys = {}; Map<String, DeviceKeys> deviceKeys = {};
Map<String, CrossSigningKey> crossSigningKeys = {}; Map<String, CrossSigningKey> crossSigningKeys = {};
SignedKey getKey(String id) {
if (deviceKeys.containsKey(id)) {
return deviceKeys[id];
}
if (crossSigningKeys.containsKey(id)) {
return crossSigningKeys[id];
}
return null;
}
CrossSigningKey getCrossSigningKey(String type) {
final keys = crossSigningKeys.values.where((k) => k.usage.contains(type));
if (keys.isEmpty) {
return null;
}
return keys.first;
}
CrossSigningKey get masterKey => getCrossSigningKey('master');
CrossSigningKey get selfSigningKey => getCrossSigningKey('self_signing');
CrossSigningKey get userSigningKey => getCrossSigningKey('user_signing');
DeviceKeysList.fromDb( DeviceKeysList.fromDb(
DbUserDeviceKey dbEntry, DbUserDeviceKey dbEntry,
List<DbUserDeviceKeysKey> childEntries, List<DbUserDeviceKeysKey> childEntries,
@ -73,7 +95,7 @@ class DeviceKeysList {
DeviceKeysList(this.userId); DeviceKeysList(this.userId);
} }
abstract class _SignedKey { abstract class SignedKey {
Client client; Client client;
String userId; String userId;
String identifier; String identifier;
@ -106,7 +128,7 @@ abstract class _SignedKey {
} }
} }
String _getSigningContent() { String get signingContent {
final data = Map<String, dynamic>.from(content); final data = Map<String, dynamic>.from(content);
// some old data might have the custom verified and blocked keys // some old data might have the custom verified and blocked keys
data.remove('verified'); data.remove('verified');
@ -121,7 +143,7 @@ abstract class _SignedKey {
final olmutil = olm.Utility(); final olmutil = olm.Utility();
var valid = false; var valid = false;
try { try {
olmutil.ed25519_verify(pubKey, _getSigningContent(), signature); olmutil.ed25519_verify(pubKey, signingContent, signature);
valid = true; valid = true;
} catch (_) { } catch (_) {
// bad signature // bad signature
@ -152,7 +174,7 @@ abstract class _SignedKey {
continue; continue;
} }
final keyId = fullKeyId.substring('ed25519:'.length); final keyId = fullKeyId.substring('ed25519:'.length);
_SignedKey key; SignedKey key;
if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) { if (client.userDeviceKeys[otherUserId].deviceKeys.containsKey(keyId)) {
key = client.userDeviceKeys[otherUserId].deviceKeys[keyId]; key = client.userDeviceKeys[otherUserId].deviceKeys[keyId];
} else if (client.userDeviceKeys[otherUserId].crossSigningKeys } else if (client.userDeviceKeys[otherUserId].crossSigningKeys
@ -204,6 +226,10 @@ abstract class _SignedKey {
return false; return false;
} }
Future<void> setVerified(bool newVerified);
Future<void> setBlocked(bool newBlocked);
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = Map<String, dynamic>.from(content); final data = Map<String, dynamic>.from(content);
// some old data may have the verified and blocked keys which are unneeded now // some old data may have the verified and blocked keys which are unneeded now
@ -216,19 +242,21 @@ abstract class _SignedKey {
String toString() => json.encode(toJson()); String toString() => json.encode(toJson());
} }
class CrossSigningKey extends _SignedKey { class CrossSigningKey extends SignedKey {
String get publicKey => identifier; String get publicKey => identifier;
List<String> usage; List<String> usage;
bool get isValid => bool get isValid =>
userId != null && publicKey != null && keys != null && ed25519Key != null; userId != null && publicKey != null && keys != null && ed25519Key != null;
@override
Future<void> setVerified(bool newVerified) { Future<void> setVerified(bool newVerified) {
_verified = newVerified; _verified = newVerified;
return client.database?.setVerifiedUserCrossSigningKey( return client.database?.setVerifiedUserCrossSigningKey(
newVerified, client.id, userId, publicKey); newVerified, client.id, userId, publicKey);
} }
@override
Future<void> setBlocked(bool newBlocked) { Future<void> setBlocked(bool newBlocked) {
blocked = newBlocked; blocked = newBlocked;
return client.database?.setBlockedUserCrossSigningKey( return client.database?.setBlockedUserCrossSigningKey(
@ -267,7 +295,7 @@ class CrossSigningKey extends _SignedKey {
} }
} }
class DeviceKeys extends _SignedKey { class DeviceKeys extends SignedKey {
String get deviceId => identifier; String get deviceId => identifier;
List<String> algorithms; List<String> algorithms;
Map<String, dynamic> unsigned; Map<String, dynamic> unsigned;
@ -281,12 +309,14 @@ class DeviceKeys extends _SignedKey {
curve25519Key != null && curve25519Key != null &&
ed25519Key != null; ed25519Key != null;
@override
Future<void> setVerified(bool newVerified) { Future<void> setVerified(bool newVerified) {
_verified = newVerified; _verified = newVerified;
return client.database return client.database
?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId); ?.setVerifiedUserDeviceKey(newVerified, client.id, userId, deviceId);
} }
@override
Future<void> setBlocked(bool newBlocked) { Future<void> setBlocked(bool newBlocked) {
blocked = newBlocked; blocked = newBlocked;
for (var room in client.rooms) { for (var room in client.rooms) {

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
import 'package:canonical_json/canonical_json.dart'; import 'package:canonical_json/canonical_json.dart';
import 'package:pedantic/pedantic.dart';
import 'package:olm/olm.dart' as olm; import 'package:olm/olm.dart' as olm;
import 'device_keys_list.dart'; import 'device_keys_list.dart';
import '../client.dart'; import '../client.dart';
@ -44,6 +45,7 @@ import '../room.dart';
enum KeyVerificationState { enum KeyVerificationState {
askAccept, askAccept,
askSSSS,
waitingAccept, waitingAccept,
askSas, askSas,
waitingSas, waitingSas,
@ -103,6 +105,8 @@ class KeyVerification {
_KeyVerificationMethod method; _KeyVerificationMethod method;
List<String> possibleMethods; List<String> possibleMethods;
Map<String, dynamic> startPaylaod; Map<String, dynamic> startPaylaod;
String _nextAction;
List<SignedKey> _verifiedDevices;
DateTime lastActivity; DateTime lastActivity;
String lastStep; String lastStep;
@ -130,7 +134,7 @@ class KeyVerification {
: null); : null);
} }
Future<void> start() async { Future<void> sendStart() async {
if (room == null) { if (room == null) {
transactionId = transactionId =
randomString(512) + DateTime.now().millisecondsSinceEpoch.toString(); randomString(512) + DateTime.now().millisecondsSinceEpoch.toString();
@ -141,6 +145,17 @@ class KeyVerification {
}); });
startedVerification = true; startedVerification = true;
setState(KeyVerificationState.waitingAccept); setState(KeyVerificationState.waitingAccept);
lastActivity = DateTime.now();
}
Future<void> start() async {
if (client.crossSigning.enabled &&
!(await client.crossSigning.isCached())) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'request';
} else {
await sendStart();
}
} }
Future<void> handlePayload(String type, Map<String, dynamic> payload, Future<void> handlePayload(String type, Map<String, dynamic> payload,
@ -228,6 +243,29 @@ class KeyVerification {
} }
} }
Future<void> openSSSS(
{String password, String recoveryKey, bool skip = false}) async {
final next = () {
if (_nextAction == 'request') {
sendStart();
} else if (_nextAction == 'done') {
if (_verifiedDevices != null) {
// and now let's sign them all in the background
client.crossSigning.sign(_verifiedDevices);
}
setState(KeyVerificationState.done);
}
};
if (skip) {
next();
return;
}
final handle = client.ssss.open('m.cross_signing.user_signing');
await handle.unlock(password: password, recoveryKey: recoveryKey);
await handle.maybeCacheAll();
next();
}
/// called when the user accepts an incoming verification /// called when the user accepts an incoming verification
Future<void> acceptVerification() async { Future<void> acceptVerification() async {
if (!(await verifyLastStep( if (!(await verifyLastStep(
@ -293,8 +331,8 @@ class KeyVerification {
} }
Future<void> verifyKeys(Map<String, String> keys, Future<void> verifyKeys(Map<String, String> keys,
Future<bool> Function(String, dynamic) verifier) async { Future<bool> Function(String, SignedKey) verifier) async {
final verifiedDevices = <String>[]; _verifiedDevices = <SignedKey>[];
if (!client.userDeviceKeys.containsKey(userId)) { if (!client.userDeviceKeys.containsKey(userId)) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
@ -304,53 +342,48 @@ class KeyVerification {
final keyId = entry.key; final keyId = entry.key;
final verifyDeviceId = keyId.substring('ed25519:'.length); final verifyDeviceId = keyId.substring('ed25519:'.length);
final keyInfo = entry.value; final keyInfo = entry.value;
if (client.userDeviceKeys[userId].deviceKeys final key = client.userDeviceKeys[userId].getKey(verifyDeviceId);
.containsKey(verifyDeviceId)) { if (key != null) {
if (!(await verifier(keyInfo, if (!(await verifier(keyInfo, key))) {
client.userDeviceKeys[userId].deviceKeys[verifyDeviceId]))) {
await cancel('m.key_mismatch'); await cancel('m.key_mismatch');
return; return;
} }
verifiedDevices.add(verifyDeviceId); _verifiedDevices.add(key);
} else if (client.userDeviceKeys[userId].crossSigningKeys
.containsKey(verifyDeviceId)) {
// this is a cross signing key!
if (!(await verifier(keyInfo,
client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId]))) {
await cancel('m.key_mismatch');
return;
}
verifiedDevices.add(verifyDeviceId);
} }
} }
// okay, we reached this far, so all the devices are verified! // okay, we reached this far, so all the devices are verified!
var verifiedMasterKey = false; var verifiedMasterKey = false;
final verifiedUserDevices = <DeviceKeys>[]; for (final key in _verifiedDevices) {
for (final verifyDeviceId in verifiedDevices) { await key.setVerified(true);
if (client.userDeviceKeys[userId].deviceKeys if (key is CrossSigningKey && key.usage.contains('master')) {
.containsKey(verifyDeviceId)) { verifiedMasterKey = true;
final key = client.userDeviceKeys[userId].deviceKeys[verifyDeviceId];
await key.setVerified(true);
verifiedUserDevices.add(key);
} else if (client.userDeviceKeys[userId].crossSigningKeys
.containsKey(verifyDeviceId)) {
final key =
client.userDeviceKeys[userId].crossSigningKeys[verifyDeviceId];
await key.setVerified(true);
if (key.usage.contains('master')) {
verifiedMasterKey = true;
}
} }
} }
if (verifiedMasterKey) { if (verifiedMasterKey && userId == client.userID) {
if (userId == client.userID) { // it was our own master key, let's request the cross signing keys
// it was our own master key, let's request the cross signing keys // we do it in the background, thus no await needed here
// we do it in the background, thus no await needed here unawaited(client.ssss.maybeRequestAll(
client.ssss.maybeRequestAll(verifiedUserDevices); _verifiedDevices.whereType<DeviceKeys>().toList()));
}
await send('m.key.verification.done', {});
var askingSSSS = false;
if (client.crossSigning.enabled &&
client.crossSigning.signable(_verifiedDevices)) {
// these keys can be signed! Let's do so
if (await client.crossSigning.isCached()) {
// and now let's sign them all in the background
unawaited(client.crossSigning.sign(_verifiedDevices));
} else { } else {
// it was someone elses master key, let's sign it askingSSSS = true;
} }
} }
if (askingSSSS) {
setState(KeyVerificationState.askSSSS);
_nextAction = 'done';
} else {
setState(KeyVerificationState.done);
}
} }
Future<bool> verifyActivity() async { Future<bool> verifyActivity() async {
@ -729,22 +762,10 @@ class _KeyVerificationMethodSas extends _KeyVerificationMethod {
mac[entry.key] = entry.value; mac[entry.key] = entry.value;
} }
} }
await request.verifyKeys(mac, (String mac, dynamic device) async { await request.verifyKeys(mac, (String mac, SignedKey key) async {
if (device is DeviceKeys) { return mac ==
return mac == _calculateMac(key.ed25519Key, baseInfo + 'ed25519:' + key.identifier);
_calculateMac(
device.ed25519Key, baseInfo + 'ed25519:' + device.deviceId);
} else if (device is CrossSigningKey) {
return mac ==
_calculateMac(
device.ed25519Key, baseInfo + 'ed25519:' + device.publicKey);
}
return false;
}); });
await request.send('m.key.verification.done', {});
if (request.state != KeyVerificationState.error) {
request.setState(KeyVerificationState.done);
}
} }
String _makeCommitment(String pubKey, String canonicalJson) { String _makeCommitment(String pubKey, String canonicalJson) {