From b15da58154287e7668cc67f7676e4af42f6da8a0 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 24 Jul 2022 23:44:05 +0800 Subject: [PATCH] feat: v0.1.0 --- Cargo.lock | 66 +++++++++++++++++++ Cargo.toml | 1 + src/cli.rs | 181 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/jose.rs | 28 +++++--- src/serve.rs | 49 ++++++++++++-- 5 files changed, 307 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77a55c7..7c24477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.1.0" @@ -105,6 +114,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -114,6 +132,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -217,6 +255,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -409,6 +457,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha2", "tokio", "zeroize", ] @@ -696,6 +745,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -884,6 +944,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "unicode-ident" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index b03e4f9..ccb12ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ zeroize = "1.5.7" clap = "2.33" hex = "0.4" base64 = "0.13.0" +sha2 = "0.10.2" lazy_static = "1.4.0" serde_derive = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/src/cli.rs b/src/cli.rs index fb60d75..ae567c1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,14 @@ +use std::io::Write; + use clap::{App, Arg, ArgMatches, SubCommand}; -use rust_util::simple_error; +use hyper::{Body, Client, Method, Request, Response, StatusCode}; +use hyper::body::Buf; +use josekit::jwk::Jwk; +use rust_util::{debugging, opt_value_result, simple_error, success, XResult}; use rust_util::util_clap::{Command, CommandError}; +use serde_json::{json, Value}; + +use crate::jose; pub struct CommandImpl; @@ -9,10 +17,175 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("Local mini KMS cli") - .arg(Arg::with_name("connect").long("connect").takes_value(true).default_value("127.0.0.1:6567").help("Connect server")) + .arg(Arg::with_name("connect").long("connect").takes_value(true).default_value("127.0.0.1:5567").help("Connect server")) + .arg(Arg::with_name("init").long("init").help("Init server")) + .arg(Arg::with_name("offline-init").long("offline-init").help("Offline init server")) + .arg(Arg::with_name("encrypt").long("encrypt").help("Encrypt text")) + .arg(Arg::with_name("decrypt").long("decrypt").help("Decrypt text")) + .arg(Arg::with_name("value").long("value").takes_value(true).help("Value, for encrypt or decrypt")) + .arg(Arg::with_name("value-hex").long("value-hex").takes_value(true).help("Value(hex), for encrypt")) + .arg(Arg::with_name("value-base64").long("value-base64").takes_value(true).help("Value(base64), for encrypt")) } - fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError { - simple_error!("Not implemented") + fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { + let init = sub_arg_matches.is_present("init"); + let offline_init = sub_arg_matches.is_present("offline-init"); + let encrypt = sub_arg_matches.is_present("encrypt"); + let decrypt = sub_arg_matches.is_present("decrypt"); + let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error"); + if init { + rt.block_on(async { + do_init(arg_matches, sub_arg_matches).await + }) + } else if offline_init { + do_offline_init(arg_matches, sub_arg_matches) + } else if encrypt { + rt.block_on(async { + do_encrypt(arg_matches, sub_arg_matches).await + }) + } else if decrypt { + rt.block_on(async { + do_decrypt(arg_matches, sub_arg_matches).await + }) + } else { + simple_error!("Need a flag") + } } +} + +async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); + + let client = Client::new(); + let uri = format!("http://{}/status", connect); + debugging!("Request uri: {}", &uri); + let req = Request::builder().method(Method::GET).uri(uri).body(Body::empty())?; + let req_response = client.request(req).await?; + if req_response.status() != StatusCode::OK { + return simple_error!("Server status is not success: {}", req_response.status().as_u16()); + } + let data = response_to_value(req_response).await?; + debugging!("Get status: {}", &data); + let status = &data["status"]; + if let Some(status) = status.as_str() { + if status == "ready" { + success!("Server is already init"); + return Ok(Some(0)); + } + if status != "not-ready" { + return simple_error!("Server status is NOT not-ready"); + } + } + let instance_public_key_jwk = &data["instance_public_key_jwk"]; + println!("Instance server public key JWK: {}", instance_public_key_jwk); + + let line = read_line("Input encrypted master key: ")?; + let uri = format!("http://{}/init", connect); + debugging!("Request uri: {}", &uri); + let body = json!({ + "encrypted_master_key": line, + }); + let body = serde_json::to_string(&body)?; + let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; + + let req_response = client.request(req).await?; + if req_response.status() != StatusCode::OK { + let status = req_response.status().as_u16(); + let data = response_to_value(req_response).await?; + return simple_error!("Server status is not success: {}, response: {}", status, data); + } + success!("Init finished"); + Ok(Some(0)) +} + +async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); + let value = sub_arg_matches.value_of("value"); + let value_hex = sub_arg_matches.value_of("value-hex"); + let value_base64 = sub_arg_matches.value_of("value-base64"); + + let client = Client::new(); + let uri = format!("http://{}/encrypt", connect); + debugging!("Request uri: {}", &uri); + let body = if let Some(value) = value { + json!({ "value": value }) + } else if let Some(value_hex) = value_hex { + json!({ "value_hex": value_hex }) + } else if let Some(value_base64) = value_base64 { + json!({ "value_base64": value_base64 }) + } else { + return simple_error!("Require one of value, value-hex, value-base64"); + }; + let body = serde_json::to_string(&body)?; + let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; + + let req_response = client.request(req).await?; + if req_response.status() != StatusCode::OK { + let status = req_response.status().as_u16(); + let data = response_to_value(req_response).await?; + return simple_error!("Server status is not success: {}, response: {}", status, data); + } + let data = response_to_value(req_response).await?; + success!("Encrypted value: {}", data["encrypted_value"].as_str().expect("Get encrypted_value error")); + Ok(Some(0)) +} + +async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); + let value = opt_value_result!(sub_arg_matches.value_of("value"), "Argument value required"); + + let client = Client::new(); + let uri = format!("http://{}/decrypt", connect); + debugging!("Request uri: {}", &uri); + let body = json!({ "encrypted_value": value }); + let body = serde_json::to_string(&body)?; + let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; + + let req_response = client.request(req).await?; + if req_response.status() != StatusCode::OK { + let status = req_response.status().as_u16(); + let data = response_to_value(req_response).await?; + return simple_error!("Server status is not success: {}, response: {}", status, data); + } + let data = response_to_value(req_response).await?; + success!("Encrypted value(hex): {}", data["value_hex"].as_str().expect("Get value_hex error")); + success!("Encrypted value(base64): {}", data["value_base64"].as_str().expect("Get value_base64 error")); + success!("Encrypted value: {}", String::from_utf8_lossy(&hex::decode(data["value_hex"].as_str().expect("Get value_hex error"))?).to_string()); + Ok(Some(0)) +} + +fn do_offline_init(_arg_matches: &ArgMatches<'_>, _sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let line = read_line("Input master key: ")?; + let master_key = if line.starts_with("hex:") { + let hex: String = line.chars().skip(4).collect(); + hex::decode(&hex)? + } else if line.starts_with("base64:") { + let base64: String = line.chars().skip(7).collect(); + base64::decode(&base64)? + } else { + line.as_bytes().to_vec() + }; + let jwk = read_line("Input JWK: ")?; + let jwk = Jwk::from_bytes(jwk.as_bytes())?; + let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &jwk)?; + + success!("Encrypted master key: {}", encrypted_master_key); + Ok(Some(0)) +} + +fn read_line(prompt: &str) -> XResult { + std::io::stdout().write(prompt.as_bytes()).ok(); + std::io::stdout().flush().ok(); + let mut line = String::new(); + if let Err(e) = std::io::stdin().read_line(&mut line) { + return simple_error!("Read from terminal failed: {}", e); + } + Ok(line.trim().to_string()) +} + +async fn response_to_value(response: Response) -> XResult { + let req_body = response.into_body(); + let whole_body = hyper::body::aggregate(req_body).await?; + let data: Value = serde_json::from_reader(whole_body.reader())?; + Ok(data) } \ No newline at end of file diff --git a/src/jose.rs b/src/jose.rs index a6ca29c..ae42240 100644 --- a/src/jose.rs +++ b/src/jose.rs @@ -6,6 +6,7 @@ use josekit::jwk::alg::rsa::RsaKeyPair; use josekit::jwk::Jwk; use rust_util::XResult; use serde_json::Value; +use sha2::Digest; const LOCAL_KMS_PREFIX: &'static str = "LKMS:"; @@ -13,23 +14,24 @@ pub fn generate_rsa_key(bits: u32) -> XResult { Ok(RsaKeyPair::generate(bits)?) } -// pub fn serialize_jwe_rsa(payload: &[u8], jwk: &Jwk) -> XResult { -// let mut header = JweHeader::new(); -// header.set_content_encryption("A256GCM"); -// header.set_claim("vendor", Some(Value::String("local-mini-kms".to_string())))?; -// let encrypter = RsaesJweAlgorithm::RsaOaep.encrypter_from_jwk(&jwk)?; -// Ok(format!("{}{}", LOCAL_KMS_PREFIX, jwe::serialize_compact(payload, &header, &encrypter)?)) -// } +pub fn serialize_jwe_rsa(payload: &[u8], jwk: &Jwk) -> XResult { + let mut header = JweHeader::new(); + header.set_content_encryption("A256GCM"); + header.set_claim("vendor", Some(Value::String("local-mini-kms".to_string())))?; + let encrypter = RsaesJweAlgorithm::RsaOaep.encrypter_from_jwk(&jwk)?; + Ok(format!("{}{}", LOCAL_KMS_PREFIX, jwe::serialize_compact(payload, &header, &encrypter)?)) +} pub fn deserialize_jwe_rsa(jwe: &str, jwk: &Jwk) -> XResult<(Vec, JweHeader)> { let decrypter = RsaesJweAlgorithm::RsaOaep.decrypter_from_jwk(jwk)?; - Ok(jwe::deserialize_json(&get_jwe(jwe), &decrypter)?) + Ok(jwe::deserialize_compact(&get_jwe(jwe), &decrypter)?) } pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult { let mut header = JweHeader::new(); header.set_content_encryption("A256GCM"); header.set_claim("vendor", Some(Value::String("local-mini-kms".to_string())))?; + header.set_claim("version", Some(Value::String(get_master_key_checksum(key))))?; let encrypter = AeskwJweAlgorithm::A256kw.encrypter_from_bytes(key)?; Ok(format!("{}{}", LOCAL_KMS_PREFIX, jwe::serialize_compact(payload, &header, &encrypter)?)) } @@ -39,6 +41,16 @@ pub fn deserialize_jwe_aes(jwe: &str, key: &[u8]) -> XResult<(Vec, JweHeader Ok(jwe::deserialize_compact(&get_jwe(jwe), &decrypter)?) } +fn get_master_key_checksum(key: &[u8]) -> String { + let digest = sha2::Sha256::digest(&key); + let digest = sha2::Sha256::digest(&digest.as_slice()); + let digest = sha2::Sha256::digest(&digest.as_slice()); + let digest = sha2::Sha256::digest(&digest.as_slice()); + let digest = sha2::Sha256::digest(&digest.as_slice()); + let digest = sha2::Sha256::digest(&digest.as_slice()); + hex::encode(&digest[0..8]) +} + fn get_jwe(jwe: &str) -> String { if jwe.starts_with(LOCAL_KMS_PREFIX) { jwe.chars().skip(LOCAL_KMS_PREFIX.len()).collect() diff --git a/src/serve.rs b/src/serve.rs index 852867a..f90661d 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -7,10 +7,10 @@ use hyper::client::HttpConnector; use hyper::service::{make_service_fn, service_fn}; use josekit::jwk::alg::rsa::RsaKeyPair; use josekit::jwk::KeyPair; -use rust_util::{debugging, failure_and_exit, information, opt_result, opt_value_result, simple_error, success, XResult}; +use rust_util::{debugging, failure_and_exit, information, opt_result, simple_error, success, XResult}; use rust_util::util_clap::{Command, CommandError}; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use zeroize::Zeroize; use crate::{db, jose}; @@ -165,10 +165,42 @@ impl MultipleViewValue { } } + +#[derive(Serialize, Deserialize)] +struct DecryptRequest { + encrypted_value: String, +} + async fn decrypt(req: Request) -> Result> { + do_response!(inner_decrypt(req).await) +} + +async fn inner_decrypt(req: Request) -> XResult<(StatusCode, Value)> { let whole_body = hyper::body::aggregate(req).await?; - let data: serde_json::Value = serde_json::from_reader(whole_body.reader())?; - Ok(Response::builder().body(format!("{}", data).into())?) + let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?; + + debugging!("To be decrypted value: {}", &data.encrypted_value); + let mut key = match get_master_key() { + None => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "status_not_ready" }))), + Some(key) => key, + }; + let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key); + key.zeroize(); + + decrypted_value.map(|v| { + let v = MultipleViewValue::from(&v.0); + let mut map = Map::new(); + if let Some(v) = &v.value { + map.insert("value".to_string(), Value::String(v.to_string())); + } + if let Some(v) = &v.value_hex { + map.insert("value_hex".to_string(), Value::String(v.to_string())); + } + if let Some(v) = &v.value_base64 { + map.insert("value_base64".to_string(), Value::String(v.to_string())); + } + (StatusCode::OK, Value::Object(map)) + }) } async fn encrypt(req: Request) -> Result> { @@ -179,7 +211,10 @@ async fn inner_encrypt(req: Request) -> XResult<(StatusCode, Value)> { let whole_body = hyper::body::aggregate(req).await?; let data: MultipleViewValue = serde_json::from_reader(whole_body.reader())?; let value = data.to_bytes()?; - let mut key = opt_value_result!( get_master_key(), "Server is not init"); + let mut key = match get_master_key() { + None => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "status_not_ready" }))), + Some(key) => key, + }; let encrypt_result = jose::serialize_jwe_aes(&value, &key); key.zeroize(); @@ -230,6 +265,7 @@ async fn inner_init(req: Request) -> XResult<(StatusCode, Value)> { } else if let Some(clear_master_key_hex) = init_request.clear_master_key_hex { hex::decode(clear_master_key_hex)? } else if let Some(encrypted_master_key) = init_request.encrypted_master_key { + debugging!("Received encrypted master key: {}", encrypted_master_key); if let Some(k) = &*startup_rw_lock { let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair.to_jwk_private_key())?; clear_master_key @@ -262,6 +298,7 @@ async fn inner_init(req: Request) -> XResult<(StatusCode, Value)> { } information!("Set master key success"); k.master_key = Some(clear_master_key); + k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?; } Ok((StatusCode::OK, json!({}))) } @@ -278,10 +315,10 @@ async fn inner_status() -> XResult<(StatusCode, Value)> { None => json!({ "status": "not-ready", "instance_public_key_jwk": memory_key.instance_rsa_key_pair.to_jwk_key_pair().to_public_key()?, + "instance_public_key_pem": String::from_utf8_lossy(&memory_key.instance_rsa_key_pair.to_pem_public_key()).to_string(), }), Some(_) => json!({ "status": "ready", - "instance_public_key_jwk": memory_key.instance_rsa_key_pair.to_jwk_key_pair().to_public_key()?, }), } };