diff --git a/Cargo.lock b/Cargo.lock index ab49bb995f..d744744f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "dirs" version = "1.0.5" @@ -1109,6 +1118,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1380,6 +1399,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -1415,6 +1437,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + [[package]] name = "lock_api" version = "0.4.2" @@ -1569,6 +1597,35 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-bigint" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-bigint-dig" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" +dependencies = [ + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits 0.2.14", + "rand 0.8.3", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1616,6 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg 1.0.1", + "libm", ] [[package]] @@ -1733,6 +1791,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2224,6 +2293,26 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rsa" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" +dependencies = [ + "byteorder", + "digest", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits 0.2.14", + "pem", + "rand 0.8.3", + "simple_asn1", + "subtle", + "zeroize", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -2484,6 +2573,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +[[package]] +name = "simple_asn1" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc31e6cf34ad4321d3a2b8f934949b429e314519f753a77962f16c664dca8e13" +dependencies = [ + "chrono", + "num-bigint", + "num-traits 0.2.14", + "thiserror", +] + [[package]] name = "simplecss" version = "0.2.0" @@ -2545,6 +2646,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2563,6 +2670,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + [[package]] name = "svg_fmt" version = "0.4.1" @@ -2600,6 +2713,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -2784,6 +2909,12 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e00391c1f3d171490a3f8bd79999b0002ae38d3da0d6a3a306c754b053d71b" +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -3059,6 +3190,7 @@ dependencies = [ "parking_lot", "postage", "rand 0.8.3", + "rsa", "rust-embed", "seahash", "serde 1.0.125", @@ -3073,4 +3205,36 @@ dependencies = [ "tree-sitter-rust", "unindent", "url", + "zed-rpc", +] + +[[package]] +name = "zed-rpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "rand 0.8.3", + "rsa", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 02ae3d61a4..fcced6e93b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["zed", "gpui", "gpui_macros", "fsevent", "scoped_pool"] +members = ["zed", "zed-rpc", "gpui", "gpui_macros", "fsevent", "scoped_pool"] [patch.crates-io] async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } diff --git a/zed-rpc/Cargo.toml b/zed-rpc/Cargo.toml new file mode 100644 index 0000000000..a07f7af3d9 --- /dev/null +++ b/zed-rpc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "zed-rpc" +description = "Shared logic for communication between the Zed app and the zed.dev server" +edition = "2018" +version = "0.1.0" + +[dependencies] +anyhow = "1.0" +base64 = "0.13" +rsa = "0.4" +rand = "0.8" diff --git a/zed-rpc/src/auth.rs b/zed-rpc/src/auth.rs new file mode 100644 index 0000000000..9136c754e3 --- /dev/null +++ b/zed-rpc/src/auth.rs @@ -0,0 +1,122 @@ +use std::convert::{TryFrom, TryInto}; + +use anyhow::{Context, Result}; +use rand::{rngs::OsRng, Rng as _}; +use rsa::{PublicKey as _, PublicKeyEncoding, RSAPrivateKey, RSAPublicKey}; + +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 = OsRng; + let bits = 1024; + 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 = OsRng; + let mut token_bytes = [0; 48]; + for byte in token_bytes.iter_mut() { + *byte = rng.gen(); + } + base64::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) -> Result { + let mut rng = OsRng; + 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_string = base64::encode(&encrypted_bytes); + Ok(encrypted_string) + } +} + +impl PrivateKey { + /// Decrypt a base64-encoded string that was encrypted by the correspoding public key. + pub fn decrypt_string(&self, encrypted_string: &str) -> Result { + let encrypted_bytes = + base64::decode(encrypted_string).context("failed to base64-decode encrypted string")?; + let bytes = self + .0 + .decrypt(PADDING_SCHEME, &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 TryInto for PublicKey { + type Error = anyhow::Error; + fn try_into(self) -> Result { + let bytes = self + .0 + .to_pkcs1() + .context("failed to serialize public key")?; + let string = base64::encode(&bytes); + Ok(string) + } +} + +impl TryFrom for PublicKey { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + let bytes = base64::decode(&value).context("failed to base64-decode public key string")?; + let key = Self(RSAPublicKey::from_pkcs1(&bytes).context("failed to parse public key")?); + Ok(key) + } +} + +const PADDING_SCHEME: rsa::PaddingScheme = rsa::PaddingScheme::PKCS1v15Encrypt; + +#[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 = public.try_into().unwrap(); + + // SERVER: + // * parse the public key + // * generate a random token. + // * encrypt the token using the public key. + let public: PublicKey = public_string.try_into().unwrap(); + let token = random_token(); + let encrypted_token = public.encrypt_string(&token).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); + } + + fn assert_printable(token: &str) { + for c in token.chars() { + assert!( + c.is_ascii_graphic(), + "token {:?} has non-printable char {}", + token, + c + ); + } + } +} diff --git a/zed-rpc/src/lib.rs b/zed-rpc/src/lib.rs new file mode 100644 index 0000000000..0e4a05d597 --- /dev/null +++ b/zed-rpc/src/lib.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 8bd748767b..ce8367d1d3 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -31,6 +31,7 @@ num_cpus = "1.13.0" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" +rsa = "0.4" rust-embed = "5.9.0" seahash = "4.1" serde = { version = "1", features = ["derive"] } @@ -42,6 +43,7 @@ toml = "0.5" tree-sitter = "0.19.5" tree-sitter-rust = "0.19.0" url = "2.2" +zed-rpc = { path = "../zed-rpc" } [dev-dependencies] cargo-bundle = "0.5.0" diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 13f3fcecf6..d40cedf480 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,3 +1,5 @@ +use std::convert::TryInto; + use anyhow::{anyhow, Context}; use gpui::MutableAppContext; use smol::io::{AsyncBufReadExt, AsyncWriteExt}; @@ -33,15 +35,18 @@ fn authenticate(_: &(), cx: &mut MutableAppContext) { let zed_url = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); let platform = cx.platform().clone(); - dbg!(&zed_url); - let task = cx.background_executor().spawn(async move { let listener = smol::net::TcpListener::bind("127.0.0.1:0").await?; let port = listener.local_addr()?.port(); + let (public_key, private_key) = + zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); + + let public_key_string: String = public_key.try_into().unwrap(); + platform.open_url(&format!( - "{}/sign_in?native_app_port={}&native_app_public_key=unused-for-now", - zed_url, port, + "{}/sign_in?native_app_port={}&native_app_public_key={}", + zed_url, port, public_key_string )); let (mut stream, _) = listener.accept().await?; @@ -54,13 +59,13 @@ fn authenticate(_: &(), cx: &mut MutableAppContext) { if let Some(path) = parts.next() { let url = Url::parse(&format!("http://example.com{}", path)) .context("failed to parse login notification url")?; + let mut user_id = None; let mut access_token = None; - let mut public_key = None; for (key, value) in url.query_pairs() { if key == "access_token" { access_token = Some(value); - } else if key == "public_key" { - public_key = Some(value); + } else if key == "user_id" { + user_id = Some(value); } } stream @@ -69,10 +74,13 @@ fn authenticate(_: &(), cx: &mut MutableAppContext) { .context("failed to write login response")?; stream.flush().await.context("failed to flush tcp stream")?; - eprintln!( - "logged in. access_token: {:?}, public_key: {:?}", - access_token, public_key - ); + if let Some((user_id, access_token)) = user_id.zip(access_token) { + let access_token = private_key.decrypt_string(&access_token); + eprintln!( + "logged in. user_id: {}, access_token: {:?}", + user_id, access_token + ); + } platform.activate(true); return Ok(());