Skip to content

E2E encryption for private chats #366

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

Open
wants to merge 1 commit into
base: master
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
5 changes: 4 additions & 1 deletion chat_sample/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'src/chat_details_screen.dart';
import 'src/chat_dialog_screen.dart';
import 'src/chat_dialog_resizable_screen.dart';
import 'src/login_screen.dart';
import 'src/managers/e2e_encryption_manager.dart';
import 'src/managers/push_notifications_manager.dart';
import 'src/select_dialog_screen.dart';
import 'src/settings_screen.dart';
Expand Down Expand Up @@ -227,7 +228,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
user.password = facebookAuthSession.token;
}
}
CubeChatConnection.instance.login(user);
CubeChatConnection.instance.login(user).then((cubeUser){
E2EEncryptionManager.instance.init();
});
} else {
CubeChatConnection.instance.markActive();
}
Expand Down
35 changes: 31 additions & 4 deletions chat_sample/lib/src/chat_dialog_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:universal_io/io.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';

import 'managers/chat_manager.dart';
import 'managers/e2e_encryption_manager.dart';
import 'update_dialog_flow.dart';
import 'utils/api_utils.dart';
import 'utils/consts.dart';
Expand Down Expand Up @@ -227,10 +228,14 @@ class ChatScreenState extends State<ChatScreen> {
});
}

void onReceiveMessage(CubeMessage message) {
Future<void> onReceiveMessage(CubeMessage message) async {
log("onReceiveMessage message= $message");
if (message.dialogId != widget.cubeDialog.dialogId) return;

if (widget.cubeDialog.isEncrypted ?? false) {
message = await E2EEncryptionManager.instance.decryptMessage(message);
}

addMessageToListView(message);
}

Expand Down Expand Up @@ -352,7 +357,19 @@ class ChatScreenState extends State<ChatScreen> {
void onSendMessage(CubeMessage message) async {
log("onSendMessage message= $message");
textEditingController.clear();
await widget.cubeDialog.sendMessage(message);

if (widget.cubeDialog.isEncrypted ?? false) {
await widget.cubeDialog.sendMessage(await E2EEncryptionManager.instance
.encryptMessage(
message,
widget.cubeDialog.dialogId!,
widget.cubeDialog.occupantsIds!
.where((userId) => userId != widget.cubeUser.id)
.first));
} else {
await widget.cubeDialog.sendMessage(message);
}

message.senderId = widget.cubeUser.id;
addMessageToListView(message);
listScrollController.animateTo(0.0,
Expand Down Expand Up @@ -1008,9 +1025,14 @@ class ChatScreenState extends State<ChatScreen> {

return getMessages(
widget.cubeDialog.dialogId!, params.getRequestParameters())
.then((result) {
.then((result) async {
lastPartSize = result!.items.length;

if (widget.cubeDialog.isEncrypted ?? false) {
return await E2EEncryptionManager.instance
.decryptMessages(result.items);
}

return result.items;
})
.whenComplete(() {})
Expand All @@ -1031,7 +1053,12 @@ class ChatScreenState extends State<ChatScreen> {

return getMessages(
widget.cubeDialog.dialogId!, params.getRequestParameters())
.then((result) {
.then((result) async {
if (widget.cubeDialog.isEncrypted ?? false) {
return await E2EEncryptionManager.instance
.decryptMessages(result!.items);
}

return result!.items;
});
}
Expand Down
2 changes: 2 additions & 0 deletions chat_sample/lib/src/login_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:universal_io/io.dart';

import '../firebase_options.dart';
import 'managers/e2e_encryption_manager.dart';
import 'managers/push_notifications_manager.dart';
import 'phone_auth_flow.dart';
import 'utils/api_utils.dart';
Expand Down Expand Up @@ -677,6 +678,7 @@ class LoginPageState extends State<LoginPage> {
CubeChatConnection.instance.login(user).then((cubeUser) {
_isLoginContinues = false;
_goDialogScreen(context, cubeUser);
E2EEncryptionManager.instance.init();
}).catchError((error) {
_processLoginError(error);
});
Expand Down
277 changes: 277 additions & 0 deletions chat_sample/lib/src/managers/e2e_encryption_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import 'dart:async';
import 'dart:convert';

import 'package:connectycube_sdk/connectycube_sdk.dart';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class E2EEncryptionManager {
static E2EEncryptionManager? _instance;

StreamSubscription<CubeMessage>? systemMessagesSubscription;

E2EEncryptionManager._();

static E2EEncryptionManager get instance =>
_instance ??= E2EEncryptionManager._();

final _secureStorage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true));
final keyAlgorithm = X25519();

init() {
_initCubeChat();
}

Future<void> initKeyExchangeForUserDialog(String dialogId, int userId) async {
final keyPair = await keyAlgorithm.newKeyPair();
final publicKey = await keyPair.extractPublicKey();

var publicKeyString = base64Encode(publicKey.bytes);

saveKeyPairForUserDialog(dialogId, userId, keyPair);

var systemMessage = CubeMessage()
..recipientId = userId
..properties = {
'exchangeType': 'request',
'publicKey': publicKeyString,
'secretDialogId': dialogId
};

CubeChatConnection.instance.systemMessagesManager
?.sendSystemMessage(systemMessage);
}

void _initCubeChat() {
if (CubeChatConnection.instance.isAuthenticated()) {
_initChatListeners();
} else {
CubeChatConnection.instance.connectionStateStream.listen((state) {
if (CubeChatConnectionState.Ready == state) {
_initChatListeners();
}
});
}
}

_initChatListeners() {
systemMessagesSubscription = CubeChatConnection
.instance.systemMessagesManager?.systemMessagesStream
.listen(onSystemMessageReceived);
}

Future<Map<String, String>> encrypt(SecretKey secretKey, String text) async {
final algorithm = AesCtr.with256bits(
macAlgorithm: Hmac.sha256(),
);

final secretBox = await algorithm.encrypt(
utf8.encode(text),
secretKey: secretKey,
);

return {
'nonce': base64Encode(secretBox.nonce),
'content': base64Encode(secretBox.cipherText),
'mac': base64Encode(secretBox.mac.bytes)
};
}

Future<String> decrypt(
SecretKey secretKey, Map<String, String> secretBox) async {
final algorithm = AesCtr.with256bits(
macAlgorithm: Hmac.sha256(),
);

var incomingSecretBox = SecretBox(
base64Decode(secretBox['content']!),
nonce: base64Decode(secretBox['nonce']!),
mac: Mac(
base64Decode(secretBox['mac']!),
),
);

return algorithm
.decrypt(
incomingSecretBox,
secretKey: secretKey,
)
.then((raw) {
return utf8.decode(raw);
});
}

Future<void> onSystemMessageReceived(CubeMessage systemMessage) async {
var senderId = systemMessage.senderId;
var secretDialogId = systemMessage.properties['secretDialogId'];
var publicKeyString = systemMessage.properties['publicKey'];

if ((secretDialogId?.isEmpty ?? true) ||
(publicKeyString?.isEmpty ?? true)) {
return;
}

var exchangeType = systemMessage.properties['exchangeType'];
var publicKey = SimplePublicKey(base64Decode(publicKeyString!),
type: KeyPairType.x25519);

if (exchangeType == 'request') {
final keyPair = await keyAlgorithm.newKeyPair();

final secretKey = await keyAlgorithm.sharedSecretKey(
keyPair: keyPair,
remotePublicKey: publicKey,
);

saveSecretKeyForUserDialog(secretKey, secretDialogId!, senderId!);
// save the same key for the current user to allow decryption of own messages if needed
// in this sample used for decryption of own messages received through API request
// it can be ignored in a real app if messages aren't stored on the backend
saveSecretKeyForUserDialog(secretKey, secretDialogId,
CubeChatConnection.instance.currentUser!.id!);

final responsePublicKey = await keyPair.extractPublicKey();

var responseSystemMessage = CubeMessage()
..recipientId = senderId
..properties = {
'exchangeType': 'response',
'publicKey': base64Encode(responsePublicKey.bytes),
'secretDialogId': secretDialogId
};

CubeChatConnection.instance.systemMessagesManager
?.sendSystemMessage(responseSystemMessage);
} else if (exchangeType == 'response') {
var keyPairForUserDialog =
await getKeyPairForUserDialog(secretDialogId!, senderId!);

if (keyPairForUserDialog != null) {
final secretKey = await keyAlgorithm.sharedSecretKey(
keyPair: keyPairForUserDialog,
remotePublicKey: publicKey,
);

saveSecretKeyForUserDialog(secretKey, secretDialogId, senderId);
// save the same key for the current user to allow decryption of own messages if needed
// in this sample used for decryption of own messages received through API request
// it can be ignored in a real app if messages aren't stored on the backend
saveSecretKeyForUserDialog(secretKey, secretDialogId,
CubeChatConnection.instance.currentUser!.id!);
}
}
}

Future<void> saveSecretKeyForUserDialog(
SecretKey secretKeyData, String dialogId, int userId) async {
final secretKeyBytes = await secretKeyData.extractBytes();
await _secureStorage.write(
key: '${userId}_${dialogId}_secretKey',
value: base64Encode(secretKeyBytes));
}

Future<SecretKeyData?> getSecretKeyForUserDialog(
String dialogId, int userId) async {
final secretKeyBase64 =
await _secureStorage.read(key: '${userId}_${dialogId}_secretKey');

if (secretKeyBase64 == null) {
return null;
}

final secretKeyBytes = base64Decode(secretKeyBase64);
return SecretKeyData(secretKeyBytes);
}

Future<void> saveKeyPairForUserDialog(
String dialogId, int userId, SimpleKeyPair keyPair) async {
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
final publicKeyBytes = (await keyPair.extractPublicKey()).bytes;

await _secureStorage.write(
key: '${userId}_${dialogId}_privateKey',
value: base64Encode(privateKeyBytes));
await _secureStorage.write(
key: '${userId}_${dialogId}_publicKey',
value: base64Encode(publicKeyBytes));
}

Future<SimpleKeyPair?> getKeyPairForUserDialog(
String dialogId, int userId) async {
final privateKeyBase64 =
await _secureStorage.read(key: '${userId}_${dialogId}_privateKey');
final publicKeyBase64 =
await _secureStorage.read(key: '${userId}_${dialogId}_publicKey');

if (privateKeyBase64 == null || publicKeyBase64 == null) {
return null;
}

final privateKeyBytes = base64Decode(privateKeyBase64);
final publicKeyBytes = base64Decode(publicKeyBase64);

final publicKey = SimplePublicKey(publicKeyBytes, type: KeyPairType.x25519);

return SimpleKeyPairData(privateKeyBytes,
publicKey: publicKey, type: KeyPairType.x25519);
}

void destroy() {
systemMessagesSubscription?.cancel();

_secureStorage.deleteAll(
aOptions: const AndroidOptions(encryptedSharedPreferences: true));
}

Future<CubeMessage> encryptMessage(
CubeMessage originalMessage, String dialogId, int userId) async {
var userDialogSecretKey = await getSecretKeyForUserDialog(dialogId, userId);

if (userDialogSecretKey == null) return originalMessage;

return encrypt(userDialogSecretKey, originalMessage.body!)
.then((encryptionData) {
var encryptedMessage = CubeMessage()
..messageId = originalMessage.messageId
..dialogId = originalMessage.dialogId
..body = 'Encrypted message'
..properties = {...originalMessage.properties, ...encryptionData}
..attachments = originalMessage.attachments
..dateSent = originalMessage.dateSent
..readIds = originalMessage.readIds
..deliveredIds = originalMessage.deliveredIds
..viewsCount = originalMessage.viewsCount
..recipientId = originalMessage.recipientId
..senderId = originalMessage.senderId
..markable = originalMessage.markable
..delayed = originalMessage.delayed
..saveToHistory = originalMessage.saveToHistory
..destroyAfter = originalMessage.destroyAfter
..isRead = originalMessage.isRead
..reactions = originalMessage.reactions;

return encryptedMessage;
});
}

Future<CubeMessage> decryptMessage(CubeMessage originalMessage) async {
var userDialogSecretKey = await getSecretKeyForUserDialog(
originalMessage.dialogId!, originalMessage.senderId!);

if (userDialogSecretKey == null) return originalMessage;

return decrypt(userDialogSecretKey, originalMessage.properties)
.then((decryptedBody) {
originalMessage.body = decryptedBody;
return originalMessage;
});
}

Future<List<CubeMessage>> decryptMessages(
List<CubeMessage> originalMessages) {
return Future.wait(originalMessages
.map((originalMessage) => decryptMessage(originalMessage))
.toList());
}
}
Loading