use std::io::Write; use base64::Engine; use base64::engine::general_purpose::STANDARD; use clap::{App, Arg, ArgMatches, SubCommand}; 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, Map, Value}; use crate::jose; 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")) } 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 { return simple_error!("Requires value hex or value base64"); } 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 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_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?; success!("Value: {}", 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?; success!("Value: {}", 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?; success!("Value: {}", 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?; success!("Value: {}", serde_json::to_string_pretty(&data)?); 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(); STANDARD.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_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()) } 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 }