diff --git a/Cargo.lock b/Cargo.lock index fb406b4..95b7f82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,7 +1039,7 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "local-mini-kms" -version = "1.0.8" +version = "1.0.9" dependencies = [ "aes-gcm-stream", "aes-kw", diff --git a/Cargo.toml b/Cargo.toml index 5335e05..a126f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "local-mini-kms" -version = "1.0.8" +version = "1.0.9" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 0a25e8a..5346087 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ Init with Yubikey: local-mini-kms serve [--init-encrypted-master-key LKMS:*** [--yubikey-challenge *challenge*]] ``` +## Local Client init via SSH + +```shell +local-mini-kms cli --init --ssh-remote root@example.com [--read-from-pinentry] +``` + ## Local Client ```shell diff --git a/src/cli.rs b/src/cli.rs index 1837202..a2558f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use hyper::body::Buf; use hyper::{Body, Client, Method, Request, Response, StatusCode}; use rust_util::util_clap::{Command, CommandError}; -use rust_util::{debugging, opt_result, opt_value_result, simple_error, success, XResult}; +use rust_util::{debugging, iff, opt_result, opt_value_result, simple_error, success, XResult}; use serde_json::{json, Map, Value}; use crate::jose; @@ -34,6 +34,8 @@ impl Command for CommandImpl { .arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge")) .arg(Arg::with_name("comment").long("comment").takes_value(true).help("Comment")) .arg(Arg::with_name("force-write").long("force-write").short("F").help("Force write value")) + .arg(Arg::with_name("read-from-pinentry").long("read-from-pinentry").help("Read from pin-entry")) + .arg(Arg::with_name("ssh-remote").long("ssh-remote").takes_value(true).help("SSH remote, root@example or localhost")) } fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { @@ -104,17 +106,16 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc } async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let ssh_remote = sub_arg_matches.value_of("ssh-remote").map(|s| s.to_string()); let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); + let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry"); - let client = Client::new(); + // 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?; + + let data = send_kms_request_with_ssh_enabled(&ssh_remote, true, &uri, &None).await?; + debugging!("Get status: {}", &data); let status = &data["status"]; if let Some(status) = status.as_str() { @@ -129,25 +130,81 @@ async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_> 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 line = { + let line = read_line("Input clear(starts with hex: or base64:) or encrypted master key: ", read_from_pinentry)?; + if line.starts_with("hex:") || line.starts_with("base64:") { + let jwk = opt_result!(serde_json::to_string(&instance_public_key_jwk), "Serialize instance server public key JWK: {} failed"); + master_key_encrypt(&line, &jwk)? + } else { + line + } + }; 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); - } + let _ = send_kms_request_with_ssh_enabled(&ssh_remote, false, &uri, &Some(body)).await?; + success!("Init finished"); Ok(Some(0)) } +async fn send_kms_request_with_ssh_enabled(ssh_remote: &Option, get_request: bool, uri: &str, body: &Option) -> XResult { + match ssh_remote { + None => { + let client = Client::new(); + let method = iff!(get_request, Method::GET, Method::POST); + let request_body = match body { + None => Body::empty(), + Some(body) => Body::from(body.clone()), + }; + let req = Request::builder().method(method).uri(uri).body(request_body)?; + 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?; + Ok(data) + } + Some(ssh_remote) => { + let mut c; + if ssh_remote == "localhost" { + c = std::process::Command::new("curl"); + } else { + c = std::process::Command::new("ssh"); + c.args([ssh_remote, "curl"]); + } + c.arg("-s"); + if !get_request { + c.args(["-X", "POST"]); + } + if let Some(body) = body { + c.args(["-H", "x-body-based64-encoded:1"]); + c.args(["--data-raw", &format!("{}", STANDARD.encode(&body))]); + } + c.arg(uri); + debugging!("Run command: {:?}", c); + let output = opt_result!(c.output(), "Call: {:?} failed: {}", c); + if !output.status.success() { + return simple_error!("Call: {:?} exit with error", output); + } + debugging!("Output: {:?}", output); + let data: Value = serde_json::from_slice(&output.stdout)?; + if let Value::Object(data_map) = &data { + if let Some(error) = data_map.get("error") { + if let Value::String(error) = error { + return simple_error!("Get error: {}, details: {}", error, data); + } + } + } + Ok(data) + } + } +} + async fn do_read(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { let body = if let Some(name) = sub_arg_matches.value_of("name") { json!({ "name": name }) @@ -214,15 +271,26 @@ async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches< 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(); +fn do_offline_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { + let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry"); + + let line = read_line("Input master key: ", read_from_pinentry)?; + let jwk = read_line("Input JWK: ", read_from_pinentry)?; + + let encrypted_master_key = master_key_encrypt(&line, &jwk)?; + + success!("Encrypted master key: {}", encrypted_master_key); + Ok(Some(0)) +} + +fn master_key_encrypt(master_key: &str, jwk: &str) -> XResult { + let master_key = if master_key.starts_with("hex:") { + let hex: String = master_key.chars().skip(4).collect(); hex::decode(&hex)? - } else if line.starts_with("base64:") { - let base64: String = line.chars().skip(7).collect(); + } else if master_key.starts_with("base64:") { + let base64: String = master_key.chars().skip(7).collect(); STANDARD.decode(&base64)? - } else if line.starts_with("LKMS:") { + } else if master_key.starts_with("LKMS:") { #[cfg(feature = "yubikey")] { use crate::yubikey_hmac; @@ -230,23 +298,28 @@ fn do_offline_init(_arg_matches: &ArgMatches<'_>, _sub_arg_matches: &ArgMatches< let challenge = opt_result!( pinentry_util::read_pin(Some("Input yubikey challenge"), Some("Challenge: ")), "Read challenge failed: {}"); let derived_key = yubikey_hmac::yubikey_challenge_as_32_bytes(challenge.get_pin().as_bytes())?; - let (key, _) = jose::deserialize_jwe_aes(&line, &derived_key)?; + let (key, _) = jose::deserialize_jwe_aes(&master_key, &derived_key)?; key } #[cfg(not(feature = "yubikey"))] return simple_error!("Yubikey feature is not enabled."); } else { - line.as_bytes().to_vec() + master_key.as_bytes().to_vec() }; - let jwk = read_line("Input JWK: ")?; let rsa_public_key = jwk_to_rsa_pubic_key(&jwk)?; let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &rsa_public_key)?; - - success!("Encrypted master key: {}", encrypted_master_key); - Ok(Some(0)) + Ok(encrypted_master_key) } -fn read_line(prompt: &str) -> XResult { +fn read_line(prompt: &str, pinentry: bool) -> XResult { + if pinentry { + read_line_from_pinentry_util(prompt) + } else { + read_line_from_terminal(prompt) + } +} + +fn read_line_from_terminal(prompt: &str) -> XResult { std::io::stdout().write_all(prompt.as_bytes()).ok(); std::io::stdout().flush().ok(); let mut line = String::new(); @@ -256,6 +329,11 @@ fn read_line(prompt: &str) -> XResult { Ok(line.trim().to_string()) } +fn read_line_from_pinentry_util(prompt: &str) -> XResult { + let pin = opt_result!(pinentry_util::read_pin(Some(prompt.to_string()), Some("PIN:".to_string())), "Read from pin-entry failed: {}"); + Ok(pin.get_pin().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?; diff --git a/src/serve.rs b/src/serve.rs index 27d118c..757d552 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -140,8 +140,9 @@ async fn response_requests( let process = proc::get_process(remote_addr.port()); match process { None => log::info!( - "[{:06}] Receive request: {}, from: {}", + "[{:06}] Receive request: {} {}, from: {}", request_idx, + req.method(), req.uri(), remote_addr ), diff --git a/src/serve_common.rs b/src/serve_common.rs index 3c40418..df14b85 100644 --- a/src/serve_common.rs +++ b/src/serve_common.rs @@ -1,20 +1,41 @@ +use std::io::{Cursor, Read}; use std::sync::Mutex; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use hyper::StatusCode; +use hyper::body::Buf; +use hyper::{Body, Request, StatusCode}; use rsa::RsaPrivateKey; use rusqlite::Connection; use rust_util::{opt_result, simple_error, XResult}; use seckey::SecBytes; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize}; use serde_json::{json, Map, Value}; - +use zeroize::Zeroize; use crate::db; pub type GenericError = Box; pub type Result = std::result::Result; +pub async fn parse_request(req: Request) -> XResult +where + T: de::DeserializeOwned, +{ + let based64_encoded = req.headers().get("x-body-based64-encoded").is_some(); + let whole_body = hyper::body::aggregate(req).await?; + let mut body = Vec::::new(); + whole_body.reader().read_to_end(&mut body)?; + if based64_encoded { + let mut based64_decoded = opt_result!(STANDARD.decode(&body), "Decode request body base64 failed: {}"); + body.clear(); + body.extend_from_slice(&based64_decoded); + based64_decoded.zeroize(); + } + let req_object = serde_json::from_reader(Cursor::new(&body))?; + body.zeroize(); + Ok(req_object) +} + #[macro_export] macro_rules! do_response { ($ex: expr) => ( @@ -35,17 +56,11 @@ pub fn ok(body: Value) -> XResult<(StatusCode, Value)> { } pub fn client_error(error: &str) -> XResult<(StatusCode, Value)> { - Ok(( - StatusCode::BAD_REQUEST, - json!({ "error": error }) - )) + Ok((StatusCode::BAD_REQUEST, json!({ "error": error }))) } pub fn server_error(error: &str) -> XResult<(StatusCode, Value)> { - Ok(( - StatusCode::INTERNAL_SERVER_ERROR, - json!({ "error": error }) - )) + Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": error }))) } // pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> { @@ -104,7 +119,8 @@ impl MultipleViewValue { } } -#[macro_export] macro_rules! require_master_key { +#[macro_export] +macro_rules! require_master_key { () => { match $crate::serve_common::get_master_key() { None => return $crate::serve_common::client_error("status_not_ready"), @@ -114,7 +130,9 @@ impl MultipleViewValue { } pub fn get_master_key() -> Option { - let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error"); + let startup_rw_lock = STATUP_RW_LOCK + .lock() + .expect("Lock read startup rw lock error"); match &*startup_rw_lock { None => None, Some(k) => match &k.master_key { @@ -147,11 +165,14 @@ pub fn byte_to_multi_view_map(bytes: &[u8], with_value: bool) -> Map XResult { - let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error"); + let startup_rw_lock = STATUP_RW_LOCK + .lock() + .expect("Lock read startup rw lock error"); match &*startup_rw_lock { None => simple_error!("Db is not initiated!"), - Some(k) => { - Ok(opt_result!(db::open_db(& k.database_file), "Open db failed: {}")) - } + Some(k) => Ok(opt_result!( + db::open_db(&k.database_file), + "Open db failed: {}" + )), } } diff --git a/src/serve_datakey.rs b/src/serve_datakey.rs index 35bfc7a..872643d 100644 --- a/src/serve_datakey.rs +++ b/src/serve_datakey.rs @@ -1,9 +1,8 @@ use crate::db::Key; -use crate::serve_common::{open_local_db, Result}; +use crate::serve_common::{open_local_db, parse_request, Result}; use crate::{db, do_response, jose, require_master_key, serve_common}; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use hyper::body::Buf; use hyper::{Body, Request, Response, StatusCode}; use rand::random; use rust_util::{iff, XResult}; @@ -30,8 +29,7 @@ pub async fn generate(req: Request) -> Result> { } async fn inner_generate(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let request: DataKeyRequest = serde_json::from_reader(whole_body.reader())?; + let request: DataKeyRequest = parse_request(req).await?; log::debug!("Generate data key: {} {}", &request.r#type, &request.spec); let key = require_master_key!(); diff --git a/src/serve_encrypt_decrypt.rs b/src/serve_encrypt_decrypt.rs index 71a8c7c..0f4b513 100644 --- a/src/serve_encrypt_decrypt.rs +++ b/src/serve_encrypt_decrypt.rs @@ -1,11 +1,10 @@ -use hyper::body::Buf; use hyper::{Body, Request, Response, StatusCode}; use rust_util::XResult; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use crate::jose; -use crate::serve_common::{self, byte_to_multi_view_map, MultipleViewValue, Result}; +use crate::serve_common::{self, byte_to_multi_view_map, parse_request, MultipleViewValue, Result}; use crate::{do_response, require_master_key}; #[derive(Serialize, Deserialize)] @@ -18,8 +17,7 @@ pub async fn decrypt(req: Request) -> Result> { } async fn inner_decrypt(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?; + let data: DecryptRequest = parse_request(req).await?; log::trace!("To be decrypted value: {}", &data.encrypted_value); let key = require_master_key!(); @@ -52,8 +50,7 @@ pub async fn encrypt(req: Request) -> Result> { } 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 data: MultipleViewValue = parse_request(req).await?; let value = data.to_bytes()?; let key = require_master_key!(); let encrypt_result = jose::serialize_jwe_aes(&value, &key.read()); diff --git a/src/serve_init.rs b/src/serve_init.rs index 310688d..fe5e910 100644 --- a/src/serve_init.rs +++ b/src/serve_init.rs @@ -1,6 +1,5 @@ use base64::engine::general_purpose::STANDARD; use base64::Engine; -use hyper::body::Buf; use hyper::{Body, Request, Response, StatusCode}; use rust_util::{opt_result, XResult}; use seckey::SecBytes; @@ -10,7 +9,7 @@ use zeroize::Zeroize; use crate::db::Key; use crate::do_response; -use crate::serve_common::{self, Result}; +use crate::serve_common::{self, parse_request, Result}; #[cfg(feature = "yubikey")] use crate::yubikey_hmac; use crate::{db, jose}; @@ -28,9 +27,7 @@ pub struct InitRequest { } async fn inner_init(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let init_request: InitRequest = serde_json::from_reader(whole_body.reader())?; - + let init_request: InitRequest = parse_request(req).await?; inner_init_request(init_request).await } diff --git a/src/serve_read_write.rs b/src/serve_read_write.rs index b09c5b7..150b9f7 100644 --- a/src/serve_read_write.rs +++ b/src/serve_read_write.rs @@ -1,8 +1,7 @@ use crate::db::Key; -use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, MultipleViewValue, Result}; +use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, parse_request, MultipleViewValue, Result}; use crate::{db, jose}; use crate::{do_response, require_master_key}; -use hyper::body::Buf; use hyper::{Body, Request, Response, StatusCode}; use rust_util::XResult; use serde::{Deserialize, Serialize}; @@ -33,8 +32,7 @@ pub async fn list(req: Request) -> Result> { } async fn inner_list(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let keys_query: KeysQuery = serde_json::from_reader(whole_body.reader())?; + let keys_query: KeysQuery = parse_request(req).await?; let conn = open_local_db()?; let keys = db::list_keys( @@ -61,8 +59,7 @@ pub async fn read(req: Request) -> Result> { } async fn inner_read(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let named: Named = serde_json::from_reader(whole_body.reader())?; + let named: Named = parse_request(req).await?; let name = &named.name; if name.is_empty() { return serve_common::client_error("name_is_empty"); @@ -92,8 +89,7 @@ pub async fn write(req: Request) -> Result> { } async fn inner_write(req: Request) -> XResult<(StatusCode, Value)> { - let whole_body = hyper::body::aggregate(req).await?; - let named_value: NamedValue = serde_json::from_reader(whole_body.reader())?; + let named_value: NamedValue = parse_request(req).await?; let name = &named_value.name; if name.is_empty() { return serve_common::client_error("name_is_empty");