359 lines
16 KiB
Rust
359 lines
16 KiB
Rust
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 client = Client::new();
|
|
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)?;
|
|
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))
|
|
}
|
|
|
|
async fn send_kms_request_with_ssh_enabled(ssh_remote: &Option<String>, get_request: bool, uri: &str, body: &Option<String>) -> XResult<Value> {
|
|
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 })
|
|
} 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<String> {
|
|
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<String> {
|
|
if pinentry {
|
|
read_line_from_pinentry_util(prompt)
|
|
} else {
|
|
read_line_from_terminal(prompt)
|
|
}
|
|
}
|
|
|
|
fn read_line_from_terminal(prompt: &str) -> XResult<String> {
|
|
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<String> {
|
|
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<Body>) -> XResult<Value> {
|
|
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<Value> {
|
|
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
|
|
}
|