Skip to content

Commit

Permalink
rpc: Add support for OAEP-based encryption format (#15058)
Browse files Browse the repository at this point in the history
This PR adds support for a new encryption format for exchanging access
tokens during the authentication flow.

The new format uses Optimal Asymmetric Encryption Padding (OAEP) instead
of PKCS#1 v1.5, which is known to be vulnerable to side-channel attacks.

**Note: We are not yet encrypting access tokens using the new format, as
this is a breaking change between the client and the server. This PR
only adds support for it, and makes it so the client and server can
decrypt either format moving forward.**

This required bumping the RSA key size from 1024 bits to 2048 bits. This
is necessary to be able to encode the access token into the ciphertext
when using OAEP.

This also follows OWASP recommendations:

> If ECC is not available and RSA must be used, then ensure that the key
is at least 2048 bits.
>
> —
[source](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#algorithms)

Release Notes:

- N/A
  • Loading branch information
maxdeviant authored Jul 24, 2024
1 parent edf7f6d commit c84da37
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 13 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion crates/collab/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,21 @@ pub fn hash_access_token(token: &str) -> String {
/// Encrypts the given access token with the given public key to avoid leaking it on the way
/// to the client.
pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<String> {
use rpc::auth::EncryptionFormat;

/// The encryption format to use for the access token.
///
/// Currently we're using the original encryption format to avoid
/// breaking compatibility with older clients.
///
/// Once enough clients are capable of decrypting the newer encryption
/// format we can start encrypting with `EncryptionFormat::V1`.
const ENCRYPTION_FORMAT: EncryptionFormat = EncryptionFormat::V0;

let native_app_public_key =
rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?;
let encrypted_access_token = native_app_public_key
.encrypt_string(access_token)
.encrypt_string(access_token, ENCRYPTION_FORMAT)
.context("failed to encrypt access token with public key")?;
Ok(encrypted_access_token)
}
Expand Down
1 change: 1 addition & 0 deletions crates/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ rand.workspace = true
rsa.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
strum.workspace = true
tracing = { version = "0.1.34", features = ["log"] }
util.workspace = true
Expand Down
77 changes: 65 additions & 12 deletions crates/rpc/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
use anyhow::{Context, Result};
use rand::{thread_rng, Rng as _};
use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey};
use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use rsa::traits::PaddingScheme;
use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use sha2::Sha256;
use std::convert::TryFrom;

fn oaep_sha256_padding() -> impl PaddingScheme {
Oaep::new::<Sha256>()
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum EncryptionFormat {
/// The original encryption format.
///
/// This is using [`Pkcs1v15Encrypt`], which is vulnerable to side-channel attacks.
/// As such, we're in the process of phasing it out.
///
/// See [here](https://people.redhat.com/~hkario/marvin/) for more details.
V0,

/// The new encryption key format using Optimal Asymmetric Encryption Padding (OAEP) with a SHA-256 digest.
V1,
}

pub struct PublicKey(RsaPublicKey);

pub struct PrivateKey(RsaPrivateKey);

/// Generate a public and private key for asymmetric encryption.
pub fn keypair() -> Result<(PublicKey, PrivateKey)> {
let mut rng = thread_rng();
let bits = 1024;
let bits = 2048;
let private_key = RsaPrivateKey::new(&mut rng, bits)?;
let public_key = RsaPublicKey::from(&private_key);
Ok((PublicKey(public_key), PrivateKey(private_key)))
Expand All @@ -30,13 +50,14 @@ pub fn random_token() -> String {
impl PublicKey {
/// Convert a string to a base64-encoded string that can only be decoded with the corresponding
/// private key.
pub fn encrypt_string(&self, string: &str) -> Result<String> {
pub fn encrypt_string(&self, string: &str, format: EncryptionFormat) -> Result<String> {
let mut rng = thread_rng();
let bytes = string.as_bytes();
let encrypted_bytes = self
.0
.encrypt(&mut rng, PADDING_SCHEME, bytes)
.context("failed to encrypt string with public key")?;
let encrypted_bytes = match format {
EncryptionFormat::V0 => self.0.encrypt(&mut rng, Pkcs1v15Encrypt, bytes),
EncryptionFormat::V1 => self.0.encrypt(&mut rng, oaep_sha256_padding(), bytes),
}
.context("failed to encrypt string with public key")?;
let encrypted_string = base64::encode_config(&encrypted_bytes, base64::URL_SAFE);
Ok(encrypted_string)
}
Expand All @@ -49,7 +70,12 @@ impl PrivateKey {
.context("failed to base64-decode encrypted string")?;
let bytes = self
.0
.decrypt(PADDING_SCHEME, &encrypted_bytes)
.decrypt(oaep_sha256_padding(), &encrypted_bytes)
.or_else(|_err| {
// If we failed to decrypt using the new format, try decrypting with the old
// one to handle mismatches between the client and server.
self.0.decrypt(Pkcs1v15Encrypt, &encrypted_bytes)
})
.context("failed to decrypt string with private key")?;
let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?;
Ok(string)
Expand Down Expand Up @@ -78,8 +104,6 @@ impl TryFrom<String> for PublicKey {
}
}

const PADDING_SCHEME: Pkcs1v15Encrypt = Pkcs1v15Encrypt;

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -99,7 +123,34 @@ mod tests {
// * encrypt the token using the public key.
let public = PublicKey::try_from(public_string).unwrap();
let token = random_token();
let encrypted_token = public.encrypt_string(&token).unwrap();
let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V1).unwrap();
assert_eq!(token.len(), 64);
assert_ne!(encrypted_token, token);
assert_printable(&token);
assert_printable(&encrypted_token);

// CLIENT:
// * decrypt the token using the private key.
let decrypted_token = private.decrypt_string(&encrypted_token).unwrap();
assert_eq!(decrypted_token, token);
}

#[test]
fn test_generate_encrypt_and_decrypt_token_with_v0_encryption_format() {
// CLIENT:
// * generate a keypair for asymmetric encryption
// * serialize the public key to send it to the server.
let (public, private) = keypair().unwrap();
let public_string = String::try_from(public).unwrap();
assert_printable(&public_string);

// SERVER:
// * parse the public key
// * generate a random token.
// * encrypt the token using the public key.
let public = PublicKey::try_from(public_string).unwrap();
let token = random_token();
let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V0).unwrap();
assert_eq!(token.len(), 64);
assert_ne!(encrypted_token, token);
assert_printable(&token);
Expand Down Expand Up @@ -130,7 +181,9 @@ mod tests {
for _ in 0..5 {
let token = random_token();
let (public_key, _) = keypair().unwrap();
let encrypted_token = public_key.encrypt_string(&token).unwrap();
let encrypted_token = public_key
.encrypt_string(&token, EncryptionFormat::V1)
.unwrap();
let public_key_str = String::try_from(public_key).unwrap();

assert_printable(&token);
Expand Down

0 comments on commit c84da37

Please sign in to comment.