Implementing end-to-end encryption (E2EE) to a Planning Poker game
Tags: crypto, e2ee, planning, scrum
Published: 2024-04-02 14:12:27

[Planning Poker®](https://www.mountaingoatsoftware.com/agile/planning-poker) is a consensus-based technique for agile estimating. It is a fun and engaging way for teams to apply relative estimates to planned work. In this blog post, we're diving into how we can enhance the security of Planning Poker sessions using end-to-end encryption (E2EE).

The code of the Qwikens **Free Planning Poker** application is open-source. 🚀🥳 <br/>
Take a look at the repository:

https://github.com/qwikens/planning-poker

### What is End-to-End Encryption?

End-to-End Encryption (E2EE) is a method of secure communication that prevents third-parties from accessing data while it's transferred from one end system or device to another. In E2EE, the data is encrypted on the sender's system or device and only the recipient is able to decrypt it. Nobody in between, be it internet service providers, hackers, or even the `platform provider itself`, can read it or tamper with it.

The principle behind E2EE is that it uses cryptographic keys to encrypt and decrypt the data. Only the communicating users possess the keys to unlock and read the messages. This ensures that the data remains confidential and integral, providing a secure channel for communication over potentially insecure networks.

### Examples of E2EE in Use

One of the most well-known examples of E2EE in use today is WhatsApp. WhatsApp encrypts messages in a way that even WhatsApp servers cannot decrypt them. This means that when you send a message, photo, video, or file, everything is encrypted automatically, and only the recipient can decrypt and view the message.

Other notable examples include:

- **Signal**: A messaging app that is widely regarded for its state-of-the-art end-to-end encryption for voice calls, video calls, and instant messaging.
- **Telegram**: Offers optional end-to-end encrypted secret chats that are not stored on their servers.
- **ProtonMail**: An email service that secures emails with end-to-end encryption, meaning that even ProtonMail cannot access the content of your emails.

These examples highlight the growing importance and implementation of E2EE in various forms of digital communication, ensuring privacy and security for users worldwide.

### Why E2EE?

Our infrastructure already provides a secure channel for communication (TLS/SSL), so why do we need E2EE?

While TLS/SSL secures the communication between the client and the server, it does not protect the data once it reaches the server. This means that the data is decrypted on the server and can potentially be accessed by the service provider or malicious actors who gain access to the server.

> More information about TLS/SSL: <br /> https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/

We are worried about the Planning Poker issues being exposed to the Qwikens's infrastructure, we want to ensure that the issues remains confidential even when it's stored on the server. This is where E2EE comes into play. By encrypting the data on the client-side before it's sent to the server, we can ensure that only the intended recipient can decrypt and access the data.

### E2EE in Planning Poker

This is not a trivial task, since we are working with a real-time collaborative tool, we need to ensure that the encryption and decryption process does not introduce significant latency or complexity to the user experience.

The first point to consider is that we are dealing with a group of participants. So, it is not enough to encrypt the data with the recipient's public key, as this would only allow the recipient to decrypt the data.

We need to encrypt the data with the public keys of all participants, so that all participants can decrypt the data.

The second point to consider is that we are dealing with a real-time application. This means that we need to ensure that the encryption and decryption process is fast enough to keep up with the pace of the planning poker game. We need to carefully consider the encryption algorithm and key size to ensure that the process is efficient and secure.

Based on these **constraints**, we had to analyze some approaches.

#### Peer-to-peer encryption

Each participant encrypts the data with the public keys of all participants and sends the encrypted data to all participants. This approach is not scalable, as the number of messages grows quadratically with the number of participants.

In the context of Planning Poker, where the number of participants is relatively small, this approach could be feasible. However, it introduces additional complexity and overhead, as each participant needs to encrypt the data multiple times and create multiple issues. This could lead to synchronization issues and potential performance problems.

#### Shared key

All participants share a **symmetric key** that is used to encrypt and decrypt the data. This approach is simpler and more efficient, as it avoids the need to encrypt the data multiple times and create multiple encrypted issues.

- When creating a room, the client generates a key and shares it with all participants.

Although this approach is simpler and more efficient, since the data is encrypted with a single key, there are two main drawbacks:

- **Single point of failure**: If the key is compromised, all data is exposed. This means that the security of the system relies on the secrecy of the key;
- **Key management**: Since all participants share the same key, there is a risk that the key could be lost or compromised. This could lead to data loss or exposure. Also, the key is a huge string, so it is not easy to share.

  - A possible solution is to share the key within the URL, as a query parameter, but this results in a very long URL, which is not ideal, since it could be truncated by some services (like Google Meet, for example).

#### Hybrid approach

What about a hybrid approach? 🤔

The idea is to combine the benefits of both approaches:

- Do not need to share keys "manually";
- Do not need to maintain multiple encrypted issues state.

The idea is to use a **symmetric key** to encrypt the data, but the key is encrypted with the public keys of all participants. This way, only the participants can decrypt the key and access the data.

- When creating a room, the user generates a pair of keys (public and private) and shares the public key;

```ts
/**
 * Generates a RSA key pair using a specified bit length.
 * @returns An object containing the PEM encoded public and private keys.
 * @example
 * const keyPair = generateKeyPair();
 * console.log(keyPair.publicKey); // PEM encoded public key
 * console.log(keyPair.privateKey); // PEM encoded private key
 */
export const generateKeyPair = async (): Promise<{
  publicKey: string;
  privateKey: string;
}> =>
  new Promise((resolve, reject) => {
    forge.pki.rsa.generateKeyPair({ bits: 2048 }, (err, keys) => {
      if (err) {
        return reject(err);
      }

      return resolve({
        publicKey: forge.pki.publicKeyToPem(keys.publicKey),
        privateKey: forge.pki.privateKeyToPem(keys.privateKey),
      });
    });
  });
```

<b>
A key point here is to generate the key pair inside a `Promise`. Generating the key pair synchronously could block the JS event loop's main thread, causing the application to freeze. By using a `Promise`, we ensure that the key pair is generated asynchronously, outside the main thread, and the application remains responsive.
</b>
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#never_blocking
<b>
For the sake of simplicity, we are using a 2048-bit RSA key pair. This is a common key size for RSA encryption and provides a good balance between security and performance. However, the key size can be adjusted based on the desired level of security and performance.
</b>

- The user also generates a symmetric key and encrypts it with its own public key;

```ts
/**
 * Generates a symmetric key using a specified bit length.
 * @param {number} [bitLength=256] - The bit length for the symmetric key. Default is 256 bits.
 * @returns {string} The generated symmetric key, base64 encoded.
 * @example
 * const symmetricKey = generateSymmetricKey();
 * console.log(symmetricKey); // Base64 encoded symmetric key
 */
export const generateSymmetricKey = (bitLength = 256) => {
  const bytes = bitLength / 8;
  const key = forge.random.getBytesSync(bytes);
  return forge.util.encode64(key);
};
```

- When any participant enters the room, the new participant generates a pair of keys and shares the public key;
- Then, any of the current participants encrypts the symmetric key with the new participant's public key and sends it to the server;

```ts
/**
 * Encrypts a message using a given public key.
 * @param message - The plaintext message to be encrypted.
 * @param publicKey - The PEM encoded public key used for encryption.
 * @returns The encrypted message, base64 encoded.
 * @example
 * const encryptedMessage = asymmetricEncrypt("Hello, world!", publicKey);
 * console.log(encryptedMessage); // Encrypted and base64 encoded message
 */
export const asymmetricEncrypt = (message: string, publicKey: string) => {
  const publicKeyRsa = forge.pki.publicKeyFromPem(publicKey);
  const encrypted = publicKeyRsa.encrypt(forge.util.encodeUtf8(message));
  return forge.util.encode64(encrypted);
};
```

- With the encrypted symmetric key in hands, the new participant can decrypt it with its private key and access the data;

```ts
/**
 * Decrypts an encrypted message using a given private key.
 * @param encryptedMessage - The encrypted message, base64 encoded.
 * @param privateKey - The PEM encoded private key used for decryption.
 * @returns The decrypted plaintext message.
 * @example
 * const decryptedMessage = asymmetricDecrypt(encryptedMessage, privateKey);
 * console.log(decryptedMessage); // Decrypted plaintext message
 */
export const asymmetricDecrypt = (
  encryptedMessage: string,
  privateKey: string
) => {
  const privateKeyObj = forge.pki.privateKeyFromPem(privateKey);
  const encrypted = forge.util.decode64(encryptedMessage);
  const decrypted = privateKeyObj.decrypt(encrypted);
  return forge.util.decodeUtf8(decrypted);
};
```

- The application needs to handle two states: `issues` and `decryptedIssues`;

```ts
type Issue = {
  id: string;
  storyPoints?: number;
  createdAt: number;
  createdBy: string;
  title: string;
};

const issuesState = proxy<Issue[]>([]);
const decryptedIssuesState = proxy<Issue[]>([]);
```

- Each issue is encrypted with the symmetric key before being sent to the server;

```ts
/**
 * Encrypts a message using a symmetric key.
 * @param message - The plaintext message to be encrypted.
 * @param symmetricKey - The base64 encoded symmetric key used for encryption.
 * @returns The encrypted message, base64 encoded.
 * @example
 * const encryptedMessage = symmetricEncrypt("Hello, world!", symmetricKey);
 * console.log(encryptedMessage); // Encrypted and base64 encoded message
 */
export const symmetricEncrypt = (message: string, symmetricKey: string) => {
  const key = forge.util.decode64(symmetricKey);
  const iv = forge.random.getBytesSync(16); // Initialization vector
  const cipher = forge.cipher.createCipher("AES-CBC", key);
  cipher.start({ iv: iv });
  cipher.update(forge.util.createBuffer(forge.util.encodeUtf8(message)));
  cipher.finish();
  const encrypted = cipher.output.getBytes();
  const encryptedMessageWithIv = `${iv}${encrypted}`;
  return forge.util.encode64(encryptedMessageWithIv);
};
```

- When the `issues` state is synchronized, the client decrypts the issues with the symmetric key and updates the `decryptedIssues` state.

```ts
/**
 * Decrypts an encrypted message using a symmetric key.
 * @param encryptedMessage - The encrypted message, base64 encoded.
 * @param symmetricKey - The base64 encoded symmetric key used for decryption.
 * @returns The decrypted plaintext message.
 * @example
 * const decryptedMessage = symmetricDecrypt(encryptedMessage, symmetricKey);
 * console.log(decryptedMessage); // Decrypted plaintext message
 */
export const symmetricDecrypt = (
  encryptedMessage: string,
  symmetricKey: string
) => {
  const key = forge.util.decode64(symmetricKey);
  const encryptedBytesWithIv = forge.util.decode64(encryptedMessage);
  const iv = encryptedBytesWithIv.slice(0, 16);
  const encryptedBytes = encryptedBytesWithIv.slice(16);
  const decipher = forge.cipher.createDecipher("AES-CBC", key);
  decipher.start({ iv: iv });
  decipher.update(forge.util.createBuffer(encryptedBytes));
  decipher.finish();
  return forge.util.decodeUtf8(decipher.output.getBytes());
};
```

### Diffie–Hellman key exchange

> Diffie–Hellman key exchange is a mathematical method of securely exchanging cryptographic keys over a public channel and was one of the first public-key protocols.
> https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange

The Diffie–Hellman key exchange is a method for securely exchanging cryptographic keys over a public channel. It allows two parties to agree on a shared secret key without exchanging the key directly. This is achieved by each party generating a public-private key pair and exchanging public keys. The shared secret key is then derived from the combination of the private key and the other party's public key.

In the context of Planning Poker, the Diffie–Hellman key exchange could be used to establish a shared symmetric key between all participants. This would eliminate the need for a single point of failure.

The problem with this approach is that it does not solve the problem of `N * M` issues being created, where N is the number of players and M is the number of issues.
The secret key now is the combination of pairs of keys, so the application needs to handle a `issuesState`for each pair of keys.

### Benefits of E2EE in Planning Poker

- **Privacy**: Ensures that issues remain confidential among the participants.
- **Security**: Protects against potential eavesdropping or data breaches.
- **Trust**: Builds trust among team members, knowing that their issues are secure.

### Next steps and Considerations

- **Room IDs**: A UUID is secure enough to be the room identifier?
- **Key size**: What is the ideal key size for the symmetric key and the RSA key pair?
- **Performance**: How to ensure that the encryption and decryption process is fast enough to keep up with the pace of the Planning Poker game?
- **RSA vs. ECC**: Should we consider using Elliptic Curve Cryptography (ECC) instead of RSA for key generation? ECC is known for its shorter key lengths and faster performance compared to RSA. RSA is more widely used and supported, but the key-pair generation and encryption/decryption process can be really slow.

### Conclusion

In this article, we have explored the concept of end-to-end encryption (E2EE) and its potential application in Planning Poker. We have discussed the importance of E2EE in ensuring privacy, security, and trust among team members.

We have also examined the challenges of implementing E2EE in a real-time collaborative tool like Planning Poker, such as the need to encrypt data with the public keys of all participants and the need to ensure that the encryption and decryption process is fast enough to keep up with the pace of the game.

We have proposed a hybrid approach that combines the benefits of both symmetric and asymmetric encryption. This approach involves using a symmetric key to encrypt the data, but the key is encrypted with the public keys of all participants. This ensures that only the participants can decrypt the key and access the data, eliminating the need for a single point of failure and reducing the complexity of key management.

However, there are still many considerations and potential challenges to address, such as the choice of key size, the performance of the encryption and decryption process, and the choice between RSA and ECC for key generation.

In conclusion, while E2EE can significantly enhance the security of Planning Poker, its implementation requires careful consideration and planning. It is not a one-size-fits-all solution, but rather a tool that can be tailored to meet the specific needs and constraints of each application.