Introduce zed-rpc with some shared auth utilities

Max Brunsfeld created

Change summary

Cargo.lock          | 164 +++++++++++++++++++++++++++++++++++++++++++++++
Cargo.toml          |   2 
zed-rpc/Cargo.toml  |  11 +++
zed-rpc/src/auth.rs | 122 ++++++++++++++++++++++++++++++++++
zed-rpc/src/lib.rs  |   1 
zed/Cargo.toml      |   2 
zed/src/lib.rs      |  30 +++++---
7 files changed, 320 insertions(+), 12 deletions(-)

Detailed changes

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",
 ]

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" }

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"

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<String> {
+        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<String> {
+        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<String> for PublicKey {
+    type Error = anyhow::Error;
+    fn try_into(self) -> Result<String> {
+        let bytes = self
+            .0
+            .to_pkcs1()
+            .context("failed to serialize public key")?;
+        let string = base64::encode(&bytes);
+        Ok(string)
+    }
+}
+
+impl TryFrom<String> for PublicKey {
+    type Error = anyhow::Error;
+    fn try_from(value: String) -> Result<Self> {
+        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
+            );
+        }
+    }
+}

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"

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(());