Skip to content

Commit

Permalink
feat: Add encrypted push
Browse files Browse the repository at this point in the history
This adds helper methods to set pushers, making sure that the pushers
are still up-to-date with what you want to set. Additionally, this
detects if the server supports encrypted push and the user does not
opt-out. Furthermore, there are helper methods to process / decrypt
a push payload.
  • Loading branch information
Sorunome committed Dec 31, 2021
1 parent 5a72287 commit 02e40e9
Show file tree
Hide file tree
Showing 13 changed files with 696 additions and 13 deletions.
6 changes: 5 additions & 1 deletion lib/encryption/encryption.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'cross_signing.dart';
import 'key_manager.dart';
import 'key_verification_manager.dart';
import 'olm_manager.dart';
import 'push_helper.dart';
import 'ssss.dart';
import 'utils/bootstrap.dart';

Expand All @@ -48,6 +49,7 @@ class Encryption {
late KeyVerificationManager keyVerificationManager;
late CrossSigning crossSigning;
late SSSS ssss;
late PushHelper pushHelper;

Encryption({
required this.client,
Expand All @@ -58,11 +60,13 @@ class Encryption {
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
crossSigning = CrossSigning(this);
pushHelper = PushHelper(this);
}

// initial login passes null to init a new olm account
Future<void> init(String? olmAccount) async {
Future<void> init({String? olmAccount, String? pushPrivateKey}) async {
await olmManager.init(olmAccount);
await pushHelper.init(pushPrivateKey);
_backgroundTasksRunning = true;
_backgroundTasks(); // start the background tasks
}
Expand Down
205 changes: 205 additions & 0 deletions lib/encryption/push_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Famedly Matrix SDK
* Copyright (C) 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 'dart:typed_data';

import 'package:olm/olm.dart' as olm;
import 'package:cryptography/cryptography.dart';

import '../matrix.dart';
import 'encryption.dart';

class PushHelper {
final Encryption encryption;
Client get client => encryption.client;
Uint8List? privateKey;
String? publicKey;

PushHelper(this.encryption);

/// base64 decode both padded and unpadded base64
Uint8List _b64decode(String s) {
// dart wants padded base64: https://github.com/dart-lang/sdk/issues/39510
final needEquals = (4 - (s.length % 4)) % 4;
return base64.decode(s + ('=' * needEquals));
}

/// Decrypt a given [ciphertext] and [ephemeral] key, validating the [mac]
Future<String> _pkDecrypt(
{required String ciphertext,
required String mac,
required String ephemeral}) async {
final _privateKey = privateKey;
if (_privateKey == null) {
throw Exception('No private key to decrypt with');
}

// first we do ECDH (x25519) with the ephemeral key, the resulting secret lands in `secretKey`
final x25519 = Cryptography.instance.x25519();
final secretKey = await x25519.sharedSecretKey(
keyPair: await x25519.newKeyPairFromSeed(_privateKey),
remotePublicKey:
SimplePublicKey(_b64decode(ephemeral), type: KeyPairType.x25519),
);

// next we do HKDF to get the aesKey, macKey and aesIv
final zerosalt = List.filled(32, 0);
final hmac = Hmac.sha256();
final prk = (await hmac.calculateMac(await secretKey.extractBytes(),
secretKey: SecretKey(zerosalt)))
.bytes;
final aesKey =
(await hmac.calculateMac([1], secretKey: SecretKey(prk))).bytes;
final macKey =
(await hmac.calculateMac([...aesKey, 2], secretKey: SecretKey(prk)))
.bytes;
final aesIv =
(await hmac.calculateMac([...macKey, 3], secretKey: SecretKey(prk)))
.bytes
.sublist(0, 16);

// now we calculate and compare the macs
final resMac = (await hmac.calculateMac(_b64decode(ciphertext),
secretKey: SecretKey(macKey)))
.bytes
.sublist(0, 8);
if (base64.encode(resMac).replaceAll('=', '') != mac.replaceAll('=', '')) {
throw Exception('Bad mac');
}

// finally decrypt the actual ciphertext
final aes = AesCbc.with256bits(macAlgorithm: MacAlgorithm.empty);
final decrypted = await aes.decrypt(
SecretBox(_b64decode(ciphertext), nonce: aesIv, mac: Mac.empty),
secretKey: SecretKey(aesKey));
return utf8.decode(decrypted);
}

/// Process a push payload, decrypting it based on its algorithm
Future<Map<String, dynamic>> processPushPayload(
Map<String, dynamic> data) async {
var algorithm = data.tryGet<String>('algorithm');
if (algorithm == null) {
List<Map<String, dynamic>>? devices;
if (data['devices'] is String) {
devices = json.decode(data['devices']).cast<Map<String, dynamic>>();
} else {
devices = data.tryGetList<Map<String, dynamic>>('devices');
}
if (devices != null && devices.isNotEmpty) {
algorithm = devices.first
.tryGetMap<String, dynamic>('data')
?.tryGet<String>('algorithm');
}
}
Logs().v('[Push] Using algorithm: $algorithm');
switch (algorithm) {
case 'com.famedly.curve25519-aes-sha2':
final ciphertext = data.tryGet<String>('ciphertext');
final mac = data.tryGet<String>('mac');
final ephemeral = data.tryGet<String>('ephemeral');
if (ciphertext == null || mac == null || ephemeral == null) {
throw Exception('Invalid encrypted push payload');
}
return json.decode(await _pkDecrypt(
ciphertext: ciphertext,
mac: mac,
ephemeral: ephemeral,
));
default:
return data;
}
}

/// Initialize the push helper with a [pushPrivateKey], generating a new keypair
/// if none passed or empty
Future<void> init([String? pushPrivateKey]) async {
if (pushPrivateKey != null && pushPrivateKey.isNotEmpty) {
try {
final _privateKey = base64.decode(pushPrivateKey);
final keyObj = olm.PkDecryption();
try {
publicKey = keyObj.init_with_private_key(_privateKey);
privateKey = _privateKey;
} finally {
keyObj.free();
}
} catch (e, s) {
client.onEncryptionError.add(
SdkError(
exception: e is Exception ? e : Exception(e),
stackTrace: s,
),
);
privateKey = null;
publicKey = null;
}
} else {
privateKey = null;
publicKey = null;
}
await _maybeGenerateNewKeypair();
}

/// Transmutes a pusher to add the public key and algorithm. Additionally generates a
/// new keypair, if needed
Future<Pusher> getPusher(Pusher pusher) async {
await _maybeGenerateNewKeypair();
if (privateKey == null) {
throw Exception('No private key found');
}
final newPusher = Pusher.fromJson(pusher.toJson());
newPusher.data = PusherData.fromJson(<String, dynamic>{
...newPusher.data.toJson(),
'public_key': publicKey,
'algorithm': 'com.famedly.curve25519-aes-sha2',
});
return newPusher;
}

/// Force generation of a new keypair
Future<void> generateNewKeypair() async {
try {
final keyObj = olm.PkDecryption();
try {
publicKey = keyObj.generate_key();
privateKey = keyObj.get_private_key();
} finally {
keyObj.free();
}
await client.database?.storePushPrivateKey(base64.encode(privateKey!));
} catch (e, s) {
client.onEncryptionError.add(
SdkError(
exception: e is Exception ? e : Exception(e),
stackTrace: s,
),
);
rethrow;
}
}

/// Generate a new keypair only if there is none
Future<void> _maybeGenerateNewKeypair() async {
if (privateKey != null) {
return;
}
await generateNewKeypair();
}
}
1 change: 1 addition & 0 deletions lib/matrix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export 'src/utils/image_pack_extension.dart';
export 'src/utils/matrix_file.dart';
export 'src/utils/matrix_id_string_extension.dart';
export 'src/utils/matrix_localizations.dart';
export 'src/utils/push_helper_extension.dart';
export 'src/utils/receipt.dart';
export 'src/utils/sync_update_extension.dart';
export 'src/utils/to_device_event.dart';
Expand Down
7 changes: 6 additions & 1 deletion lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,7 @@ class Client extends MatrixApi {
String? olmAccount;
String? accessToken;
String? _userID;
String? pushPrivateKey;
final account = await this.database?.getClient(clientName);
if (account != null) {
_id = account['client_id'];
Expand All @@ -1038,6 +1039,7 @@ class Client extends MatrixApi {
syncFilterId = account['sync_filter_id'];
prevBatch = account['prev_batch'];
olmAccount = account['olm_account'];
pushPrivateKey = account['push_private_key'];
}
if (newToken != null) {
accessToken = this.accessToken = newToken;
Expand Down Expand Up @@ -1079,7 +1081,10 @@ class Client extends MatrixApi {
encryption?.dispose();
encryption = null;
}
await encryption?.init(olmAccount);
await encryption?.init(
olmAccount: olmAccount,
pushPrivateKey: pushPrivateKey,
);

final database = this.database;
if (database != null) {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/database/database_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ abstract class DatabaseApi {
String syncFilterId,
);

Future storePushPrivateKey(
String? pushPrivateKey,
);

Future storeAccountData(String type, String content);

Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client);
Expand Down
11 changes: 11 additions & 0 deletions lib/src/database/fluffybox_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,17 @@ class FluffyBoxDatabase extends DatabaseApi {
await _clientBox.put('sync_filter_id', syncFilterId);
}

@override
Future<void> storePushPrivateKey(
String? pushPrivateKey,
) async {
if (pushPrivateKey == null) {
await _clientBox.delete('push_private_key');
} else {
await _clientBox.put('push_private_key', pushPrivateKey);
}
}

@override
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
String content, bool verified, bool blocked) async {
Expand Down
24 changes: 16 additions & 8 deletions lib/src/database/hive_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -684,14 +684,15 @@ class FamedlySdkHiveDatabase extends DatabaseApi {

@override
Future<void> insertClient(
String name,
String homeserverUrl,
String token,
String userId,
String? deviceId,
String? deviceName,
String? prevBatch,
String? olmAccount) async {
String name,
String homeserverUrl,
String token,
String userId,
String? deviceId,
String? deviceName,
String? prevBatch,
String? olmAccount,
) async {
await _clientBox.put('homeserver_url', homeserverUrl);
await _clientBox.put('token', token);
await _clientBox.put('user_id', userId);
Expand Down Expand Up @@ -1185,6 +1186,13 @@ class FamedlySdkHiveDatabase extends DatabaseApi {
await _clientBox.put('sync_filter_id', syncFilterId);
}

@override
Future<void> storePushPrivateKey(
String? pushPrivateKey,
) async {
await _clientBox.put('push_private_key', pushPrivateKey);
}

@override
Future<void> storeUserCrossSigningKey(String userId, String publicKey,
String content, bool verified, bool blocked) async {
Expand Down
Loading

0 comments on commit 02e40e9

Please sign in to comment.