v0.1.0-init-version #1
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,12 +1,11 @@
|
|||||||
|
local-mini-kms.db
|
||||||
|
.idea/
|
||||||
# ---> Rust
|
# ---> Rust
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|||||||
1068
Cargo.lock
generated
Normal file
1068
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "local-mini-kms"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
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"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
josekit = "0.8.1"
|
||||||
|
rust_util = { version = "0.6", features = ["use_clap"] }
|
||||||
|
tokio = { version = "1.19", features = ["full"] }
|
||||||
|
hyper = { version = "0.14.20", features = ["client", "server", "tcp", "http1", "http2"] }
|
||||||
|
rusqlite = "0.28.0"
|
||||||
191
src/cli.rs
Normal file
191
src/cli.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
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, 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").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 {
|
||||||
|
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<String> {
|
||||||
|
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<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)
|
||||||
|
}
|
||||||
57
src/db.rs
Normal file
57
src/db.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use rusqlite::{Connection, params};
|
||||||
|
use rust_util::{debugging, information, opt_result, simple_error, success, XResult};
|
||||||
|
|
||||||
|
pub const DEFAULT_MASTER_KEY_VERIFICATION_KEY: &'static str = "__master_verification_key";
|
||||||
|
|
||||||
|
pub struct Key {
|
||||||
|
pub name: String,
|
||||||
|
pub encrypted_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_db(db: &str) -> XResult<Connection> {
|
||||||
|
let con = opt_result!(Connection::open(db), "Open sqlite db: {}, failed: {}", db);
|
||||||
|
debugging!("Db auto commit: {}", con.is_autocommit());
|
||||||
|
Ok(con)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_db(conn: &Connection) -> XResult<bool> {
|
||||||
|
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='keys'")?;
|
||||||
|
let mut rows = stmt.query(())?;
|
||||||
|
if rows.next()?.is_some() {
|
||||||
|
information!("Table keys exists, skip init");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = conn.execute(r##"
|
||||||
|
CREATE TABLE keys (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
value TEXT
|
||||||
|
)
|
||||||
|
"##, ())?;
|
||||||
|
success!("Table keys created");
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> {
|
||||||
|
let _ = conn.execute(
|
||||||
|
"INSERT INTO keys (name, value) VALUES (?1, ?2)",
|
||||||
|
(&key.name, &key.encrypted_key),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT id, name, value FROM keys WHERE name = ?1")?;
|
||||||
|
let mut key_iter = stmt.query_map(params![name], |row| {
|
||||||
|
Ok(Key {
|
||||||
|
name: row.get(1)?,
|
||||||
|
encrypted_key: row.get(2)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
match key_iter.next() {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(Ok(r)) => Ok(Some(r)),
|
||||||
|
Some(Err(e)) => simple_error!("Find key failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/jose.rs
Normal file
60
src/jose.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use josekit::jwe;
|
||||||
|
use josekit::jwe::alg::aeskw::AeskwJweAlgorithm;
|
||||||
|
use josekit::jwe::alg::rsaes::RsaesJweAlgorithm;
|
||||||
|
use josekit::jwe::JweHeader;
|
||||||
|
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:";
|
||||||
|
|
||||||
|
pub fn generate_rsa_key(bits: u32) -> XResult<RsaKeyPair> {
|
||||||
|
Ok(RsaKeyPair::generate(bits)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize_jwe_rsa(payload: &[u8], jwk: &Jwk) -> XResult<String> {
|
||||||
|
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<u8>, JweHeader)> {
|
||||||
|
let decrypter = RsaesJweAlgorithm::RsaOaep.decrypter_from_jwk(jwk)?;
|
||||||
|
Ok(jwe::deserialize_compact(&get_jwe(jwe), &decrypter)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult<String> {
|
||||||
|
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)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_jwe_aes(jwe: &str, key: &[u8]) -> XResult<(Vec<u8>, JweHeader)> {
|
||||||
|
let decrypter = AeskwJweAlgorithm::A256kw.decrypter_from_bytes(key)?;
|
||||||
|
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()
|
||||||
|
} else {
|
||||||
|
jwe.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main.rs
Normal file
49
src/main.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use clap::{App, AppSettings, ArgMatches};
|
||||||
|
use rust_util::{failure_and_exit, information};
|
||||||
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
|
||||||
|
mod db;
|
||||||
|
mod jose;
|
||||||
|
mod cli;
|
||||||
|
mod serve;
|
||||||
|
|
||||||
|
pub struct DefaultCommandImpl;
|
||||||
|
|
||||||
|
impl DefaultCommandImpl {
|
||||||
|
pub fn process_command<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
pub fn run(_arg_matches: &ArgMatches) -> CommandError {
|
||||||
|
information!("Local mini KMS cli, use --help for help");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(e) = inner_main() {
|
||||||
|
failure_and_exit!("Run local-mini-kms error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_main() -> CommandError {
|
||||||
|
let commands: Vec<Box<dyn Command>> = vec![
|
||||||
|
Box::new(cli::CommandImpl),
|
||||||
|
Box::new(serve::CommandImpl),
|
||||||
|
];
|
||||||
|
let mut app = App::new(env!("CARGO_PKG_NAME"))
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||||
|
.long_about("Local mini KMS")
|
||||||
|
.setting(AppSettings::ColoredHelp);
|
||||||
|
app = DefaultCommandImpl::process_command(app);
|
||||||
|
for command in &commands {
|
||||||
|
app = app.subcommand(command.subcommand());
|
||||||
|
}
|
||||||
|
let matches = app.get_matches();
|
||||||
|
for command in &commands {
|
||||||
|
if let Some(sub_cmd_matches) = matches.subcommand_matches(command.name()) {
|
||||||
|
return command.run(&matches, sub_cmd_matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DefaultCommandImpl::run(&matches)
|
||||||
|
}
|
||||||
340
src/serve.rs
Normal file
340
src/serve.rs
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
|
use hyper::{Body, Client, Method, Request, Response, Server, StatusCode};
|
||||||
|
use hyper::body::Buf;
|
||||||
|
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, simple_error, success, XResult};
|
||||||
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{json, Map, Value};
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use crate::{db, jose};
|
||||||
|
use crate::db::Key;
|
||||||
|
|
||||||
|
type GenericError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
type Result<T> = std::result::Result<T, GenericError>;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct CommandImpl;
|
||||||
|
|
||||||
|
impl Command for CommandImpl {
|
||||||
|
fn name(&self) -> &str { "serve" }
|
||||||
|
|
||||||
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
|
SubCommand::with_name(self.name()).about("Local mini KMS serve")
|
||||||
|
.arg(Arg::with_name("listen").long("listen").takes_value(true).default_value("127.0.0.1:5567").help("Listen"))
|
||||||
|
.arg(Arg::with_name("local-db").long("local-db").takes_value(true).default_value("local-mini-kms.db").help("Local db file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||||
|
let local_mini_kms_db = sub_arg_matches.value_of("local-db").expect("Get local mini kms db error");
|
||||||
|
match init_instance(local_mini_kms_db) {
|
||||||
|
Ok(true) => success!("Init server success"),
|
||||||
|
Ok(false) => failure_and_exit!("SHOULD NOT HAPPEN, server already init"),
|
||||||
|
Err(e) => failure_and_exit!("Init server failed: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
let listen = sub_arg_matches.value_of("listen").expect("Get argument listen error");
|
||||||
|
let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error");
|
||||||
|
rt.block_on(async {
|
||||||
|
let addr = listen.parse().expect(&format!("Parse listen error: {}", listen));
|
||||||
|
let client = Client::new();
|
||||||
|
let new_service = make_service_fn(move |_| {
|
||||||
|
let client = client.clone();
|
||||||
|
async {
|
||||||
|
Ok::<_, GenericError>(service_fn(move |req| {
|
||||||
|
response_requests(req, client.to_owned())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let server = Server::bind(&addr).serve(new_service);
|
||||||
|
information!("Listening on http://{}", addr);
|
||||||
|
match server.await {
|
||||||
|
Err(e) => failure_and_exit!("Server error: {}", e),
|
||||||
|
Ok(_) => success!("Server ended"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(Some(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ref: https://github.com/hyperium/hyper/blob/master/examples/web_api.rs
|
||||||
|
// ref: https://crates.io/crates/rusqlite
|
||||||
|
async fn response_requests(
|
||||||
|
req: Request<Body>,
|
||||||
|
_client: Client<HttpConnector>,
|
||||||
|
) -> Result<Response<Body>> {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::POST, "/init") => init(req).await,
|
||||||
|
(&Method::POST, "/update") => update().await,
|
||||||
|
(&Method::POST, "/decrypt") => decrypt(req).await,
|
||||||
|
(&Method::POST, "/encrypt") => encrypt(req).await,
|
||||||
|
(&Method::GET, "/status") => status().await,
|
||||||
|
(&Method::GET, "/version") => get_version().await,
|
||||||
|
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND).body(serde_json::to_string_pretty(&json!({ "error": "not_found" }))?.into())?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! do_response {
|
||||||
|
($ex: expr) => (
|
||||||
|
match $ex {
|
||||||
|
Ok((status_code, body)) => Ok(Response::builder().status(status_code).body(serde_json::to_string_pretty(&body)?.into())?),
|
||||||
|
Err(e) => Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(serde_json::to_string_pretty(&json!({
|
||||||
|
"error": "internal_error",
|
||||||
|
"error_message": format!("{}", e),
|
||||||
|
}))?.into())?),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct MemoryKey {
|
||||||
|
database_file: String,
|
||||||
|
instance_rsa_key_pair: RsaKeyPair,
|
||||||
|
master_key: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref STATUP_RW_LOCK: RwLock<Option<MemoryKey>> = RwLock::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_instance(db: &str) -> XResult<bool> {
|
||||||
|
let conn = db::open_db(db)?;
|
||||||
|
db::init_db(&conn)?;
|
||||||
|
|
||||||
|
let mut startup_rw_lock = STATUP_RW_LOCK.write().expect("Lock write startup rw lock error");
|
||||||
|
match &*startup_rw_lock {
|
||||||
|
Some(_) => Ok(false),
|
||||||
|
None => {
|
||||||
|
let memory_key = MemoryKey {
|
||||||
|
database_file: db.to_string(),
|
||||||
|
instance_rsa_key_pair: jose::generate_rsa_key(4096)?,
|
||||||
|
master_key: None,
|
||||||
|
};
|
||||||
|
*startup_rw_lock = Some(memory_key);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_instance_rsa_key_pair() -> XResult<bool> {
|
||||||
|
let mut startup_rw_lock = STATUP_RW_LOCK.write().expect("Lock write startup rw lock error");
|
||||||
|
match &mut *startup_rw_lock {
|
||||||
|
Some(k) => {
|
||||||
|
k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
None => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct MultipleViewValue {
|
||||||
|
value: Option<String>,
|
||||||
|
value_hex: Option<String>,
|
||||||
|
value_base64: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultipleViewValue {
|
||||||
|
fn from(v: &[u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(String::from_utf8_lossy(v).to_string()),
|
||||||
|
value_hex: Some(hex::encode(v)),
|
||||||
|
value_base64: Some(base64::encode(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bytes(&self) -> XResult<Vec<u8>> {
|
||||||
|
if let Some(v) = &self.value {
|
||||||
|
Ok(v.as_bytes().to_vec())
|
||||||
|
} else if let Some(v) = &self.value_hex {
|
||||||
|
let v = opt_result!(hex::decode(v), "Decode hex failed: {}");
|
||||||
|
Ok(v)
|
||||||
|
} else if let Some(v) = &self.value_base64 {
|
||||||
|
let v = opt_result!(base64::decode(v), "Decode base64 failed: {}");
|
||||||
|
Ok(v)
|
||||||
|
} else {
|
||||||
|
simple_error!("Multiple view value is all empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct DecryptRequest {
|
||||||
|
encrypted_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn decrypt(req: Request<Body>) -> Result<Response<Body>> {
|
||||||
|
do_response!(inner_decrypt(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
|
||||||
|
let whole_body = hyper::body::aggregate(req).await?;
|
||||||
|
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<Body>) -> Result<Response<Body>> {
|
||||||
|
do_response!(inner_encrypt(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_encrypt(req: Request<Body>) -> 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 = 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();
|
||||||
|
|
||||||
|
encrypt_result.map(|e| {
|
||||||
|
(StatusCode::OK, json!({
|
||||||
|
"encrypted_value": e,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update() -> Result<Response<Body>> {
|
||||||
|
do_response!(inner_update().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_update() -> XResult<(StatusCode, Value)> {
|
||||||
|
let update = update_instance_rsa_key_pair()?;
|
||||||
|
Ok((StatusCode::OK, json!({
|
||||||
|
"update": update,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(req: Request<Body>) -> Result<Response<Body>> {
|
||||||
|
do_response!(inner_init(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct InitRequest {
|
||||||
|
clear_master_key_hex: Option<String>,
|
||||||
|
clear_master_key_base64: Option<String>,
|
||||||
|
encrypted_master_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
|
||||||
|
let whole_body = hyper::body::aggregate(req).await?;
|
||||||
|
let init_request: InitRequest = serde_json::from_reader(whole_body.reader())?;
|
||||||
|
|
||||||
|
let mut startup_rw_lock = STATUP_RW_LOCK.write().expect("Lock read startup rw lock error");
|
||||||
|
match &*startup_rw_lock {
|
||||||
|
None => return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))),
|
||||||
|
Some(memory_key) => match memory_key.master_key {
|
||||||
|
Some(_) => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "already init " }))),
|
||||||
|
None => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let clear_master_key = if let Some(clear_master_key_base64) = &init_request.clear_master_key_base64 {
|
||||||
|
base64::decode(clear_master_key_base64)?
|
||||||
|
} 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
|
||||||
|
} else {
|
||||||
|
return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " })));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "master key is not assigned" })));
|
||||||
|
};
|
||||||
|
|
||||||
|
if clear_master_key.len() != 32 {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "bad clear_master_key length" })));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = &mut *startup_rw_lock {
|
||||||
|
let conn = opt_result!(db::open_db(&k.database_file), "Open db failed: {}");
|
||||||
|
let default_master_key_verification_key = db::find_key(&conn, db::DEFAULT_MASTER_KEY_VERIFICATION_KEY)?;
|
||||||
|
match default_master_key_verification_key {
|
||||||
|
None => {
|
||||||
|
let key = Key {
|
||||||
|
name: db::DEFAULT_MASTER_KEY_VERIFICATION_KEY.to_string(),
|
||||||
|
encrypted_key: jose::serialize_jwe_aes("LOCAL-MINI-KMS:MAGIC-VERIFICATION-KEY".as_bytes(), &clear_master_key)?,
|
||||||
|
};
|
||||||
|
db::insert_key(&conn, &key)?;
|
||||||
|
}
|
||||||
|
Some(key) => {
|
||||||
|
debugging!("Found jwe: {}", &key.encrypted_key);
|
||||||
|
let _ = opt_result!(jose::deserialize_jwe_aes(&key.encrypted_key, &clear_master_key), "Deserialize master key verification key failed: {}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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!({})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn status() -> Result<Response<Body>> {
|
||||||
|
do_response!(inner_status().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_status() -> XResult<(StatusCode, Value)> {
|
||||||
|
let startup_rw_lock = STATUP_RW_LOCK.read().expect("Lock read startup rw lock error");
|
||||||
|
let body = match &*startup_rw_lock {
|
||||||
|
None => json!({ "status": "n/a" }),
|
||||||
|
Some(memory_key) => match memory_key.master_key {
|
||||||
|
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",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((StatusCode::OK, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_version() -> Result<Response<Body>> {
|
||||||
|
Ok(Response::builder().body(format!(
|
||||||
|
"{} - {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")
|
||||||
|
).into())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_master_key() -> Option<Vec<u8>> {
|
||||||
|
let startup_rw_lock = STATUP_RW_LOCK.read().expect("Lock read startup rw lock error");
|
||||||
|
match &*startup_rw_lock {
|
||||||
|
None => None,
|
||||||
|
Some(k) => k.master_key.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user