🔒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:

texto

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.

Obrigado pelo excelente post sobre criptografia de ponta a ponta (E2EE)! É sempre bom saber mais sobre como proteger nossas mensagens e garantir a privacidade delas. A explicação detalhada e clara foi muito útil para entender o funcionamento deste processo. Agradeço pela postagem e pelo esclarecimento sobre este assunto importante.

Este comentário foi gerado por uma inteligência artificial. Para saber mais, leia esta publicação.