diff --git a/docusaurus/docs/Flutter/assets/end_to_end_encryption.png b/docusaurus/docs/Flutter/assets/end_to_end_encryption.png new file mode 100644 index 00000000..b41e11ce Binary files /dev/null and b/docusaurus/docs/Flutter/assets/end_to_end_encryption.png differ diff --git a/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx new file mode 100644 index 00000000..8a85c6a5 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/end_to_end_chat_encryption.mdx @@ -0,0 +1,258 @@ +--- +id: end_to_end_chat_encryption +sidebar_position: 12 +title: End To End Chat Encryption +--- + +## 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: + +![](../assets/end_to_end_encryption.png) + +## 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 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 Crypto Key + +Next, create a symmetric **Crypto 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> 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 **Crypto 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 encryptMessage(String message, List 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 decryptMessage(String encryptedMessage, List 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 **MessageInput** 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 +MessageInput( + + ... + + 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 **MessageListView** widget to have a custom `messagebuilder`, that can decrypt the message. + +```dart +MessageListView( + ... + 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( + 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).