use std::io::Write; use base64::engine::general_purpose::STANDARD; use base64::Engine; 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, iff, opt_result, opt_value_result, simple_error, success, XResult}; use serde_json::{json, Map, Value}; use crate::jose; use crate::jose::jwk_to_rsa_pubic_key; pub struct CommandImpl; impl Command for CommandImpl { fn name(&self) -> &str { "cli" } fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("Local mini KMS cli") .arg(Arg::with_name("connect").long("connect").short("C").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("direct-init").long("direct-init").help("Direct 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("read").long("read").help("Read value")) .arg(Arg::with_name("write").long("write").help("Write value")) .arg(Arg::with_name("name").long("name").short("n").takes_value(true).help("Read/Write key name")) .arg(Arg::with_name("value").long("value").short("v").takes_value(true).help("Value, for encrypt or decrypt")) .arg(Arg::with_name("value-hex").long("value-hex").short("x").takes_value(true).help("Value(hex), for encrypt")) .arg(Arg::with_name("value-base64").long("value-base64").short("b").takes_value(true).help("Value(base64), for encrypt")) .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 { let init = sub_arg_matches.is_present("init"); let direct_init = sub_arg_matches.is_present("direct-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 read = sub_arg_matches.is_present("read"); let write = sub_arg_matches.is_present("write"); 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 direct_init { rt.block_on(async { do_direct_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 if read { rt.block_on(async { do_read(arg_matches, sub_arg_matches).await }) } else if write { rt.block_on(async { do_write(arg_matches, sub_arg_matches).await }) } else { simple_error!("Need a flag") } } } async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { let value_hex = sub_arg_matches.value_of("value-hex"); let value_base64 = sub_arg_matches.value_of("value-base64"); let yubikey_challenge = sub_arg_matches.value_of("yubikey-challenge"); let mut body_map = Map::new(); if let Some(value_hex) = value_hex { body_map.insert("clear_master_key_hex".to_string(), value_hex.into()); } else if let Some(value_base64) = value_base64 { body_map.insert("clear_master_key_base64".to_string(), value_base64.into()); } else { let pin = match pinentry_util::read_pin( Some("Input your clear master key, starts with hex: or base64:"), Some("Clear master key: ")) { Ok(pin) => pin, Err(e) => return simple_error!("Read clear master key failed: {}", e), }; let pin_str = pin.get_pin(); let clear_master_key = if pin_str.starts_with("hex:") { let hex: String = pin_str.chars().skip(4).collect(); hex::decode(&hex)? } else if pin_str.starts_with("base64:") { let base64: String = pin_str.chars().skip(7).collect(); STANDARD.decode(&base64)? } else { return simple_error!("Clear master key must starts with hex: or base64:"); }; body_map.insert("clear_master_key_hex".to_string(), hex::encode(&clear_master_key).into()); } if let Some(yubikey_challenge) = yubikey_challenge { body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into()); } let _data = do_inner_request(sub_arg_matches, "init", &Value::Object(body_map)).await?; success!("Init finished"); Ok(Some(0)) } 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 uri = format!("http://{}/status", connect); debugging!("Request uri: {}", &uri); 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() { 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 = { let line = read_line("Input clear(starts with hex: or base64:) or encrypted master key: ", read_from_pinentry)?; let line = iff!(line.starts_with("hmac_enc:"), card_hmac_decrypt(&line)?, line); 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 _ = send_kms_request_with_ssh_enabled(&ssh_remote, false, &uri, &Some(body)).await?; success!("Init finished"); Ok(Some(0)) } fn card_hmac_decrypt(ciphertext: &str) -> XResult { let mut c = std::process::Command::new("card-cli"); c.args(&["hmac-decrypt", "--ciphertext", &ciphertext, "--json"]); 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); } let data: Value = serde_json::from_slice(&output.stdout)?; if let Value::Object(data_map) = &data { if let Some(Value::String(plaintext)) = data_map.get("plaintext") { return Ok(plaintext.to_string()); } } simple_error!("Hmac decrypt without plaintext, data: {:?}", data) } 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", &STANDARD.encode(body).to_string()]); } 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(Value::String(error)) = data_map.get("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 }) } else { return simple_error!("Require key"); }; let data = do_inner_request(sub_arg_matches, "read", &body).await?; println!("{}", serde_json::to_string_pretty(&data)?); Ok(Some(0)) } async fn do_write(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { let name = if let Some(name) = sub_arg_matches.value_of("name") { name } else { return simple_error!("Require key"); }; 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 force_write = sub_arg_matches.is_present("force-write"); let comment = sub_arg_matches.value_of("comment"); let body = if let Some(value) = value { json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value": value}) }) } else if let Some(value_hex) = value_hex { json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value_hex": value_hex}) }) } else if let Some(value_base64) = value_base64 { json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value_base64": value_base64}) }) } else { return simple_error!("Require one of value, value-hex, value-base64"); }; let data = do_inner_request(sub_arg_matches, "write", &body).await?; println!("{}", serde_json::to_string_pretty(&data)?); Ok(Some(0)) } async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { 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 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 data = do_inner_request(sub_arg_matches, "encrypt", &body).await?; println!("{}", serde_json::to_string_pretty(&data)?); Ok(Some(0)) } async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { let value = opt_value_result!(sub_arg_matches.value_of("value"), "Argument value required"); let body = json!({ "encrypted_value": value }); let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?; println!("{}", serde_json::to_string_pretty(&data)?); Ok(Some(0)) } 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 master_key.starts_with("base64:") { let base64: String = master_key.chars().skip(7).collect(); STANDARD.decode(&base64)? } else if master_key.starts_with("LKMS:") { #[cfg(feature = "yubikey")] { use crate::yubikey_hmac; // Yubikey Hmac encrypted key 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(master_key, &derived_key)?; key } #[cfg(not(feature = "yubikey"))] return simple_error!("Yubikey feature is not enabled."); } else { master_key.as_bytes().to_vec() }; let rsa_public_key = jwk_to_rsa_pubic_key(jwk)?; let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &rsa_public_key)?; Ok(encrypted_master_key) } 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(); if let Err(e) = std::io::stdin().read_line(&mut line) { return simple_error!("Read from terminal failed: {}", e); } 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?; let data: Value = serde_json::from_reader(whole_body.reader())?; Ok(data) } async fn do_inner_request(sub_arg_matches: &ArgMatches<'_>, action: &str, body: &Value) -> XResult { let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); let body = serde_json::to_string(&body)?; let client = Client::new(); let uri = format!("http://{}/{}", connect, action); 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); // } response_to_value(req_response).await }