Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: feat: Add encrypted push #1242

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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