zed/crates/rpc/src/auth.rs
Nathan Sobo 1ae326432e
Extract a scheduler crate from GPUI to enable unified integration testing of client and server code (#37326)
Extracts and cleans up GPUI's scheduler code into a new `scheduler`
crate, making it pluggable by external runtimes. This will enable
deterministic integration testing with cloud components by providing a
unified test scheduler across Zed and backend code. In Zed, it will
replace the existing GPUI scheduler for consistent async task management
across platforms.

## Changes

- **Core Implementation**: `TestScheduler` with seed-based
randomization, session tracking (`SessionId`), and foreground/background
task separation for reproducible testing.
- **Executors**: `ForegroundExecutor` (!Send, thread-local) and
`BackgroundExecutor` (Send, with blocking/timeout support) as
GPUI-compatible wrappers.
- **Clock and Timer**: Controllable `TestClock` and future-based `Timer`
for time-sensitive tests.
- **Testing APIs**: `once()`, `with_seed()`, and `many()` methods for
configurable test runs.
- **Dependencies**: Added `async-task`, `chrono`, `futures`, etc., with
updates to `Cargo.toml` and lock file.

## Benefits

- **Integration Testing**: Facilitates reliable async tests involving
cloud sessions, reducing flakiness via deterministic execution.
- **Pluggability**: Trait-based design (`Scheduler`) allows easy
integration into non-GPUI runtimes while maintaining GPUI compatibility.
- **Cleanup**: Refactors GPUI scheduler logic for clarity, correctness
(no `unwrap()`, proper error handling), and extensibility.

Follows Rust guidelines; run `./script/clippy` for verification.

- [x] Define and test a core scheduler that we think can power our cloud
code and GPUI
- [ ] Replace GPUI's scheduler


Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-09-04 17:14:53 +02:00

240 lines
8.2 KiB
Rust

use anyhow::{Context as _, Result};
use base64::prelude::*;
use rand::prelude::*;
use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey};
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 = RsaRngCompat::new();
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)))
}
/// Generate a random 64-character base64 string.
pub fn random_token() -> String {
let mut rng = rand::rng();
let mut token_bytes = [0; 48];
for byte in token_bytes.iter_mut() {
*byte = rng.random();
}
BASE64_URL_SAFE.encode(token_bytes)
}
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, format: EncryptionFormat) -> Result<String> {
let mut rng = RsaRngCompat::new();
let bytes = string.as_bytes();
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_URL_SAFE.encode(&encrypted_bytes);
Ok(encrypted_string)
}
}
impl PrivateKey {
/// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
let encrypted_bytes = BASE64_URL_SAFE
.decode(encrypted_string)
.context("failed to base64-decode encrypted string")?;
let bytes = self
.0
.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)
}
}
impl TryFrom<PublicKey> for String {
type Error = anyhow::Error;
fn try_from(key: PublicKey) -> Result<Self> {
let bytes = key
.0
.to_pkcs1_der()
.context("failed to serialize public key")?;
let string = BASE64_URL_SAFE.encode(&bytes);
Ok(string)
}
}
impl TryFrom<String> for PublicKey {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self> {
let bytes = BASE64_URL_SAFE
.decode(&value)
.context("failed to base64-decode public key string")?;
let key = Self(RsaPublicKey::from_pkcs1_der(&bytes).context("failed to parse public key")?);
Ok(key)
}
}
// TODO: remove once we rsa v0.10 is released.
struct RsaRngCompat(rand::rngs::ThreadRng);
impl RsaRngCompat {
fn new() -> Self {
Self(rand::rng())
}
}
impl rsa::signature::rand_core::RngCore for RsaRngCompat {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::signature::rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
impl rsa::signature::rand_core::CryptoRng for RsaRngCompat {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_encrypt_and_decrypt_token() {
// 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::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);
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_encode_and_decode_base64_public_key() {
// A base64-encoded public key.
//
// We're using a literal string to ensure that encoding and decoding works across differences in implementations.
let encoded_public_key = "MIGJAoGBAMPvufou8wOuUIF1Wlkbtn0ZMM9nC55QJ06nTZvgMfZv5esFVU9-cQO_JC1P9ZoEcMDJweFERnQuQLqzsrMDLFbkdgL128ZU43WOLiQraxaICFIZsPUeTtWMKp2D5bPWsNxs-lnCma7vCAry6fpXuj5AKQdk7cTZJNucgvZQ0uUfAgMBAAE=".to_string();
// Make sure we can parse the public key.
let public_key = PublicKey::try_from(encoded_public_key.clone()).unwrap();
// Make sure we re-encode to the same format.
assert_eq!(encoded_public_key, String::try_from(public_key).unwrap());
}
#[test]
fn test_tokens_are_always_url_safe() {
for _ in 0..5 {
let token = random_token();
let (public_key, _) = keypair().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);
assert_printable(&public_key_str);
assert_printable(&encrypted_token);
}
}
fn assert_printable(token: &str) {
for c in token.chars() {
assert!(
c.is_ascii_graphic(),
"token {:?} has non-printable char {}",
token,
c
);
assert_ne!(c, '/', "token {:?} is not URL-safe", token);
assert_ne!(c, '&', "token {:?} is not URL-safe", token);
}
}
}