Introduce zed-rpc with some shared auth utilities

This commit is contained in:
Max Brunsfeld 2021-06-08 13:07:06 -07:00
parent db3e01a12c
commit 327c20510b
7 changed files with 320 additions and 12 deletions

164
Cargo.lock generated
View File

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

View File

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

11
zed-rpc/Cargo.toml Normal file
View File

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

122
zed-rpc/src/auth.rs Normal file
View File

@ -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
);
}
}
}

1
zed-rpc/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod auth;

View File

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

View File

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