🔒Criptografia de ponta a ponta para Flutter
O que é criptografia de ponta a ponta?
Criptografia de ponta a ponta (E2EE) é o processo de proteger uma mensagem de terceiros para que apenas o remetente e o destinatário possam acessar a mensagem. O E2EE fornece segurança armazenando a mensagem de forma criptografada no servidor ou banco de dados do aplicativo.
Você só pode acessar a mensagem descriptografando e assinando-a usando uma chave pública conhecida (distribuída gratuitamente) e uma chave privada correspondente (conhecida apenas pelo proprietário).
Cada usuário no aplicativo tem seu próprio par de chaves público-privadas. As chaves públicas são distribuídas publicamente e criptografam as mensagens do remetente. O destinatário só pode descriptografar a mensagem do remetente com a chave privada correspondente.
Confira o diagrama abaixo para um exemplo:
Configurar
Dependências
Adicione o pacote webcrypto em seu pubspec.yaml
dependencies:
webcrypto: ^0.5.2 # latest version
Gerar par de chaves
Escreva uma função que gere um par de chaves usando o algoritmo ECDH e a curva elíptica P- 256 ( P-256 é bem suportado e oferece o equilíbrio certo entre segurança e desempenho).
O par será composto por duas chaves:
PublicKey : A chave que está vinculada a um usuário para criptografar mensagens.
PrivateKey : a chave que é armazenada localmente para descriptografar mensagens.
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;
}
Gerar uma chave criptográfica
Em seguida, crie uma chave criptográfica simétrica usando as chaves geradas na etapa anterior. Você usará essas chaves para criptografar e descriptografar mensagens.
// 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;
}
Criptografia de mensagens
Depois de gerar a chave criptográfica , você estará pronto para criptografar a mensagem. Você pode usar o algoritmo AES-GCM por seu conhecido equilíbrio de segurança e desempenho e boa disponibilidade do navegador.
// 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);
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;
}
Descriptografando mensagens
Descriptografar uma mensagem é o oposto de criptografá-la. Para descriptografar uma mensagem em um formato legível por humanos, use o trecho de código abaixo:
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;
}
Implementar como um recurso de chat de transmissão
Agora que sua configuração está concluída, você pode usá-la para implementar a criptografia de ponta a ponta em seu aplicativo.
Armazenar chave pública do usuário
A primeira coisa que você precisa fazer é armazenar o gerado publicKeycomo uma extraDatapropriedade, para que outros usuários criptografem as mensagens.
// Generating keyPair using the function defined in above steps
final keyPair = generateKeys();
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,
);
Enviando mensagens criptografadas
Agora você usará a encryptMessage()função criada nas etapas anteriores para criptografar a mensagem.
Para fazer isso, você precisa fazer algumas pequenas alterações no widget StreamMessageInput .
final receiverJwk = receiver.extraData['publicKey'];
// Generating derivedKey using user's privateKey and receiver's publicKey
final derivedKey = await deriveKey(keyPair.privateKey, receiverJwk);
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é um parâmetro que permite que seu aplicativo processe a mensagem antes de ir para o servidor do Stream. Aqui, você o usou para criptografar a mensagem antes de enviá-la ao back-end do Stream.
Mostrando Mensagens Descriptografadas
Agora, é hora de descriptografar a mensagem e apresentá-la em um formato legível para o receptor.
Você pode personalizar o widget StreamMessageListView para ter um messagebuilder, que pode descriptografar a mensagem.
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,
);
},
);
},
),
É isso! Isso é tudo que você precisa para implementar o E2EE em um aplicativo de bate-papo com tecnologia Stream.