260 lines
8.2 KiB
Plaintext
260 lines
8.2 KiB
Plaintext
---
|
||
id: end_to_end_chat_encryption
|
||
title: Encryption
|
||
---
|
||
|
||
Adding End To End Encryption to your Chat App
|
||
|
||
## Introduction
|
||
|
||
When you communicate over a chat application with another person or group,
|
||
you may exchange sensitive information, like personally identifiable information, financial details, or passwords.
|
||
A chat application should use end-to-end encryption to ensure that users' data stays secure.
|
||
|
||
:::note
|
||
Before you start, keep in mind that this guide is a basic example intended for educational purposes only.
|
||
If you want to implement end-to-end encryption in your production app, please consult a security professional first.
|
||
There’s a lot more to consider from a security perspective that isn’t covered here.
|
||
:::
|
||
|
||
## What is End-to-End Encryption?
|
||
|
||
End-to-end encryption (E2EE) is the process of securing a message from third parties so that only the sender and receiver can access the message.
|
||
E2EE provides security by storing the message in an encrypted form on the application's server or database.
|
||
|
||
You can only access the message by decrypting and signing it using a known public key (distributed freely)
|
||
and a corresponding private key (only known by the owner).
|
||
|
||
Each user in the application has their own public-private key pair.
|
||
Public keys are distributed publicly and encrypt the sender’s messages.
|
||
The receiver can only decrypt the sender’s message with the matching private key.
|
||
|
||
Check out the diagram below for an example:
|
||
|
||

|
||
|
||
## Setup
|
||
|
||
### Dependencies
|
||
|
||
Add the [`webcrypto`](https://pub.dev/packages/webcrypto) package in your `pubspec.yaml` file.
|
||
|
||
```yaml
|
||
dependencies:
|
||
webcrypto: ^0.5.2 # latest version
|
||
```
|
||
|
||
### Generate Key Pair
|
||
|
||
Write a function that generates a key pair using the **ECDH** algorithm and the **P-256** elliptic curve (**P-256** is well-supported and
|
||
offers the right balance of security and performance).
|
||
|
||
The pair will consist of two keys:
|
||
- **PublicKey**: The key that is linked to a user to encrypt messages.
|
||
- **PrivateKey**: The key that is stored locally to decrypt messages.
|
||
|
||
```dart
|
||
Future<JsonWebKeyPair> generateKeys() async {
|
||
final keyPair = await EcdhPrivateKey.generateKey(EllipticCurve.p256);
|
||
final publicKeyJwk = await keyPair.publicKey.exportJsonWebKey();
|
||
final privateKeyJwk = await keyPair.privateKey.exportJsonWebKey();
|
||
|
||
return JsonWebKeyPair(
|
||
privateKey: json.encode(privateKeyJwk),
|
||
publicKey: json.encode(publicKeyJwk),
|
||
);
|
||
}
|
||
|
||
// Model class for storing keys
|
||
class JsonWebKeyPair {
|
||
const JsonWebKeyPair({
|
||
required this.privateKey,
|
||
required this.publicKey,
|
||
});
|
||
|
||
final String privateKey;
|
||
final String publicKey;
|
||
}
|
||
```
|
||
|
||
### Generate a Cryptographic Key
|
||
|
||
Next, create a symmetric **Cryptographic Key** using the keys generated in the previous step.
|
||
You will use those keys to encrypt and decrypt messages.
|
||
|
||
```dart
|
||
// SendersJwk -> sender.privateKey
|
||
// ReceiverJwk -> receiver.publicKey
|
||
Future<List<int>> deriveKey(String senderJwk, String receiverJwk) async {
|
||
// Sender's key
|
||
final senderPrivateKey = json.decode(senderJwk);
|
||
final senderEcdhKey = await EcdhPrivateKey.importJsonWebKey(
|
||
senderPrivateKey,
|
||
EllipticCurve.p256,
|
||
);
|
||
|
||
// Receiver's key
|
||
final receiverPublicKey = json.decode(receiverJwk);
|
||
final receiverEcdhKey = await EcdhPublicKey.importJsonWebKey(
|
||
receiverPublicKey,
|
||
EllipticCurve.p256,
|
||
);
|
||
|
||
// Generating CryptoKey
|
||
final derivedBits = await senderEcdhKey.deriveBits(256, receiverEcdhKey);
|
||
return derivedBits;
|
||
}
|
||
```
|
||
|
||
### Encrypting Messages
|
||
|
||
Once you have generated the **Cryptographic Key**, you're ready to encrypt the message.
|
||
You can use the **AES-GCM** algorithm for its known security and performance balance and good browser availability.
|
||
|
||
```dart
|
||
// The "iv" stands for initialization vector (IV). To ensure the encryption’s strength,
|
||
// each encryption process must use a random and distinct IV.
|
||
// It’s included in the message so that the decryption procedure can use it.
|
||
final Uint8List iv = Uint8List.fromList('Initialization Vector'.codeUnits);
|
||
```
|
||
|
||
```dart
|
||
Future<String> encryptMessage(String message, List<int> deriveKey) async {
|
||
// Importing cryptoKey
|
||
final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey);
|
||
|
||
// Converting message into bytes
|
||
final messageBytes = Uint8List.fromList(message.codeUnits);
|
||
|
||
// Encrypting the message
|
||
final encryptedMessageBytes =
|
||
await aesGcmSecretKey.encryptBytes(messageBytes, iv);
|
||
|
||
// Converting encrypted message into String
|
||
final encryptedMessage = String.fromCharCodes(encryptedMessageBytes);
|
||
return encryptedMessage;
|
||
}
|
||
```
|
||
|
||
### Decrypting Messages
|
||
|
||
Decrypting a message is the opposite of encrypting one.
|
||
To decrypt a message to a human-readable format, use the code snippet below:
|
||
|
||
```dart
|
||
Future<String> decryptMessage(String encryptedMessage, List<int> deriveKey) async {
|
||
// Importing cryptoKey
|
||
final aesGcmSecretKey = await AesGcmSecretKey.importRawKey(deriveKey);
|
||
|
||
// Converting message into bytes
|
||
final messageBytes = Uint8List.fromList(encryptedMessage.codeUnits);
|
||
|
||
// Decrypting the message
|
||
final decryptedMessageBytes =
|
||
await aesGcmSecretKey.decryptBytes(messageBytes, iv);
|
||
|
||
// Converting decrypted message into String
|
||
final decryptedMessage = String.fromCharCodes(decryptedMessageBytes);
|
||
return decryptedMessage;
|
||
}
|
||
```
|
||
|
||
## Implement as a Stream Chat Feature
|
||
|
||
Now that your setup is complete you can use it to implement end-to-end encryption in your app.
|
||
|
||
### Store User's Public Key
|
||
|
||
The first thing you need to do is store the generated `publicKey` as an `extraData` property, in order
|
||
for other users to encrypt messages.
|
||
|
||
```dart
|
||
// Generating keyPair using the function defined in above steps
|
||
final keyPair = generateKeys();
|
||
```
|
||
|
||
```dart
|
||
await client.connectUser(
|
||
User(
|
||
id: 'cool-shadow-7',
|
||
name: 'Cool Shadow',
|
||
image: 'https://getstream.io/cool-shadow',
|
||
|
||
// set publicKey as a extraData property
|
||
extraData: { 'publicKey': keyPair.publicKey },
|
||
),
|
||
client.devToken('cool-shadow-7').rawValue,
|
||
);
|
||
```
|
||
|
||
### Sending Encrypted Messages
|
||
|
||
Now you will use the `encryptMessage()` function created in the previous steps to encrypt the message.
|
||
|
||
To do that, you need to make some minor changes to the **StreamMessageInput** widget.
|
||
|
||
```dart
|
||
final receiverJwk = receiver.extraData['publicKey'];
|
||
|
||
// Generating derivedKey using user's privateKey and receiver's publicKey
|
||
final derivedKey = await deriveKey(keyPair.privateKey, receiverJwk);
|
||
```
|
||
|
||
```dart
|
||
StreamMessageInput(
|
||
|
||
...
|
||
|
||
preMessageSending: (message) async {
|
||
// Encrypting the message text using derivedKey
|
||
final encryptedMessage = await encryptMessage(message.text, derivedKey);
|
||
|
||
// Creating a new message with the encrypted message text
|
||
final newMessage = message.copyWith(text: encryptedMessage);
|
||
|
||
return newMessage;
|
||
},
|
||
),
|
||
```
|
||
|
||
`preMessageSending` is a parameter that allows your app to process the message before it goes to Stream’s server.
|
||
Here, you have used it to encrypt the message before sending it to Stream’s backend.
|
||
|
||
### Showing Decrypted Messages
|
||
|
||
Now, it’s time to decrypt the message and present it in a human-readable format to the receiver.
|
||
|
||
You can customize the **StreamMessageListView** widget to have a custom `messagebuilder`, that can decrypt the message.
|
||
|
||
```dart
|
||
StreamMessageListView(
|
||
...
|
||
messageBuilder: (context, messageDetails, currentMessages, defaultWidget) {
|
||
// Retrieving the message from details
|
||
final message = messageDetails.message;
|
||
|
||
// Decrypting the message text using the derivedKey
|
||
final decryptedMessageFuture = decryptMessage(message.text, derivedKey);
|
||
return FutureBuilder<String>(
|
||
future: decryptedMessageFuture,
|
||
builder: (context, snapshot) {
|
||
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
|
||
if (!snapshot.hasData) return Container();
|
||
|
||
// Updating the original message with the decrypted text
|
||
final decryptedMessage = message.copyWith(text: snapshot.data);
|
||
|
||
// Returning defaultWidget with updated message
|
||
return defaultWidget.copyWith(
|
||
message: decryptedMessage,
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
```
|
||
|
||
That's it. That's all you need to implement E2EE in a Stream powered chat app.
|
||
|
||
For more details, check out our [end-to-end encrypted chat article](https://getstream.io/blog/end-to-end-encrypted-chat-in-flutter/#whats-end-to-end-encryption).
|