Compare commits

...

28 Commits

Author SHA1 Message Date
120526f6af feat: add tests 2025-06-29 22:59:26 +08:00
7c760d26df feat: v1.0.10, support with hmac encrypted master key 2025-06-29 00:17:04 +08:00
a6a6674f30 feat: update dependencies 2025-06-28 12:26:35 +08:00
Hatter Jiang
dc8616d644 feat: make clippy happy 2025-06-28 12:18:57 +08:00
Hatter Jiang
9b2705966a feat: rm unused line 2025-06-28 12:14:58 +08:00
Hatter Jiang
88154c9397 feat: v1.0.9, add init via ssh 2025-06-28 12:12:16 +08:00
a8d3f6dadb feat: add zeroizing_alloc 2025-06-28 00:24:58 +08:00
4c7008ce26 feat: add justfile 2025-03-27 07:39:55 +08:00
453e064139 update dependencies 2025-03-16 17:43:04 +08:00
ffdecf0703 feat: v1.0.8, direct init secure read password 2025-03-16 17:31:32 +08:00
26daa10c23 feat: add zigbuilg 2025-01-12 14:10:02 +08:00
c9ccd35053 feat: v1.0.7, offline init support yubikey 2024-12-13 21:30:06 +08:00
660a9e305d feat: v1.0.6, support list 2024-12-12 23:31:57 +08:00
4796b53aae feat: v1.0.5, auto repair table keys 2024-11-23 23:05:22 +08:00
1cad0ee922 feat: logging 2024-11-22 23:52:22 +08:00
a4ef732a2b feat: update readme 2024-11-22 23:47:41 +08:00
aff9359172 feat: update verification key 2024-11-22 23:45:30 +08:00
87cba2be57 feat: v1.0.4, generate data key and save to db 2024-11-22 23:42:04 +08:00
20ad9e6bd7 feat: log 2024-11-21 23:18:32 +08:00
ca0f18f2bc feat: v1.0.3, log 2024-11-21 23:07:34 +08:00
7d9f9f6870 feat: v1.0.3, add log4rs 2024-11-21 23:00:37 +08:00
e2f5bc52a2 feat: fix core on CentOS 6.9 2024-11-20 02:07:22 +08:00
b29374ce1d feat: upate sqlite ver 2024-11-20 01:58:28 +08:00
c939490f0e feat: v1.0.2, add feature harden_process 2024-11-20 01:47:32 +08:00
5e6694c53e feat: update dependencies 2024-11-15 22:45:47 +08:00
909ac90eb9 feat: datakey decrypt fails when not exportable 2024-11-15 01:44:21 +08:00
655f9f5ede feat: datakey support exportable 2024-11-15 01:26:09 +08:00
82b38a2cf1 feat: v1.0.1, update datakey 2024-11-15 00:24:36 +08:00
17 changed files with 1840 additions and 560 deletions

1162
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,44 @@
[package] [package]
name = "local-mini-kms" name = "local-mini-kms"
version = "1.0.0" version = "1.0.10"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["yubikey"] default = ["yubikey", "harden_process"]
yubikey = ["yubico_manager"] yubikey = ["yubico_manager"]
harden_process = ["secmem-proc", "procfs"]
[dependencies] [dependencies]
zeroize = "1.8" zeroize = "1.8"
clap = "2.34" clap = "2.34"
hex = "0.4" hex = "0.4"
base64 = "0.21" base64 = "0.22"
sha2 = "0.10" sha2 = "0.10"
lazy_static = "1.5" lazy_static = "1.5"
serde_derive = "1.0" serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
secmem-proc = "0.3" secmem-proc = { version = "0.3", optional = true }
seckey = "0.12" seckey = "0.12"
rust_util = { version = "0.6", features = ["use_clap"] } rust_util = { version = "0.6", features = ["use_clap"] }
tokio = { version = "1.37", features = ["full"] } tokio = { version = "1.45", features = ["full"] }
hyper = { version = "0.14", features = ["client", "server", "tcp", "http1", "http2"] } hyper = { version = "0.14", features = ["client", "server", "tcp", "http1", "http2"] }
rusqlite = "0.31" # use bundled fix musl build core dump on CentOS 6.9
rusqlite = { version = "0.32", features = ["bundled"] }
yubico_manager = { version = "0.9", optional = true } yubico_manager = { version = "0.9", optional = true }
rpassword = "7.3"
rand = "0.8" rand = "0.8"
rsa = "0.9" rsa = "0.9"
aes-kw = { version = "0.2", features = ["alloc"] } aes-kw = { version = "0.2", features = ["alloc"] }
sha1 = "0.10" sha1 = "0.10"
aes-gcm-stream = "0.2" aes-gcm-stream = "0.2"
jose-jwk = "0.1" jose-jwk = "0.1"
log = "0.4"
env_logger = "0.11"
log4rs = "1.3"
pinentry-util = "0.1.1"
zeroizing-alloc = "0.1.0"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
procfs = "0.13" procfs = { version = "0.13", optional = true }

View File

@@ -11,6 +11,7 @@ cargo build --release [--no-default-features]
## Init ## Init
New random master key: New random master key:
```shell ```shell
head -c 32 /dev/random | base64 head -c 32 /dev/random | base64
``` ```
@@ -18,6 +19,7 @@ head -c 32 /dev/random | base64
## Generate Yubikey encrypted master key ## Generate Yubikey encrypted master key
Generate encrypted master key with Yubikey: Generate encrypted master key with Yubikey:
```shell ```shell
local-mini-kms yubikey-init-master-key --generate-key [--yubikey-challenge *challenge*] local-mini-kms yubikey-init-master-key --generate-key [--yubikey-challenge *challenge*]
``` ```
@@ -25,15 +27,23 @@ local-mini-kms yubikey-init-master-key --generate-key [--yubikey-challenge *chal
## Startup Server ## Startup Server
Startup without init: Startup without init:
```shell ```shell
local-mini-kms serve local-mini-kms serve
``` ```
Init with Yubikey: Init with Yubikey:
```shell ```shell
local-mini-kms serve [--init-encrypted-master-key LKMS:*** [--yubikey-challenge *challenge*]] 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 ## Local Client
```shell ```shell
@@ -67,6 +77,7 @@ local-mini-kms cli --write --name test --value hello [--force-write] [--comment
## cURL ## cURL
Write value: Write value:
```shell ```shell
curl -X POST http://127.0.0.1:5567/write \ curl -X POST http://127.0.0.1:5567/write \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -74,6 +85,7 @@ curl -X POST http://127.0.0.1:5567/write \
``` ```
Read value: Read value:
```shell ```shell
curl -X POST http://127.0.0.1:5567/read \ curl -X POST http://127.0.0.1:5567/read \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -81,14 +93,39 @@ curl -X POST http://127.0.0.1:5567/read \
``` ```
Generate data key: Generate data key:
```shell ```shell
curl -X POST http://127.0.0.1:5567/datakey \ curl -X POST http://127.0.0.1:5567/datakey \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"key_type":"aes", "key_spec":"256", "return_plaintext": true}' -d '{"type":"aes", "spec":"256", "exportable": true, "return_plaintext": true, "name": "key001", "comment": "the comment"}'
``` ```
```shell
xh POST http://127.0.0.1:5567/datakey \
type=aes \
spec=256 \
exportable:=false \
name=testkey01 \
comment='this is a test key 01'
```
```shell
xh POST http://127.0.0.1:5567/list type=value name=name limit:=10
```
| Key | Comment |
|------------------|------------------------------------------------------|
| type | `aes` |
| spec | ~~`128`, `192`,~~ `256` if `type` == `aes` |
| exportable | <i>[optional]</i> `true` or `false` , default `true` |
| return_plaintext | <i>[optional]</i> `true` or `false`, default `false` |
| name | <i>[optional]</i> Data key name |
| comment | <i>[optional]</i> Data key comment |
Upgrade to v3.2 Upgrade to v3.2
```sql ```sql
ALTER TABLE keys ADD COLUMN comment TEXT; ALTER TABLE keys
ADD COLUMN comment TEXT;
``` ```

View File

@@ -1,6 +1,10 @@
_: _:
@just --list @just --list
# publish
publish:
cargo publish --registry crates-io
check: check:
cargo check cargo check
@@ -10,3 +14,6 @@ lint:
build: build:
cargo build --release cargo build --release
build-linux-musl-with-zig:
cargo zigbuild --release --target x86_64-unknown-linux-musl --no-default-features

View File

@@ -6,7 +6,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
use hyper::body::Buf; use hyper::body::Buf;
use hyper::{Body, Client, Method, Request, Response, StatusCode}; use hyper::{Body, Client, Method, Request, Response, StatusCode};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::{debugging, 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 serde_json::{json, Map, Value};
use crate::jose; 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("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("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("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 { fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -76,7 +78,24 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc
} else if let Some(value_base64) = value_base64 { } else if let Some(value_base64) = value_base64 {
body_map.insert("clear_master_key_base64".to_string(), value_base64.into()); body_map.insert("clear_master_key_base64".to_string(), value_base64.into());
} else { } else {
return simple_error!("Requires value hex or value base64"); 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 { if let Some(yubikey_challenge) = yubikey_challenge {
body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into()); body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into());
@@ -87,17 +106,15 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc
} }
async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { 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 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); let uri = format!("http://{}/status", connect);
debugging!("Request uri: {}", &uri); debugging!("Request uri: {}", &uri);
let req = Request::builder().method(Method::GET).uri(uri).body(Body::empty())?;
let req_response = client.request(req).await?; let data = send_kms_request_with_ssh_enabled(&ssh_remote, true, &uri, &None).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); debugging!("Get status: {}", &data);
let status = &data["status"]; let status = &data["status"];
if let Some(status) = status.as_str() { if let Some(status) = status.as_str() {
@@ -112,25 +129,98 @@ async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>
let instance_public_key_jwk = &data["instance_public_key_jwk"]; let instance_public_key_jwk = &data["instance_public_key_jwk"];
println!("Instance server public key JWK: {}", 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)?;
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); let uri = format!("http://{}/init", connect);
debugging!("Request uri: {}", &uri); debugging!("Request uri: {}", &uri);
let body = json!({ let body = json!({
"encrypted_master_key": line, "encrypted_master_key": line,
}); });
let body = serde_json::to_string(&body)?; 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?; let _ = send_kms_request_with_ssh_enabled(&ssh_remote, false, &uri, &Some(body)).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"); success!("Init finished");
Ok(Some(0)) Ok(Some(0))
} }
fn card_hmac_decrypt(ciphertext: &str) -> XResult<String> {
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<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", &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 { async fn do_read(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let body = if let Some(name) = sub_arg_matches.value_of("name") { let body = if let Some(name) = sub_arg_matches.value_of("name") {
json!({ "name": name }) json!({ "name": name })
@@ -164,7 +254,7 @@ async fn do_write(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_
return simple_error!("Require one of value, value-hex, value-base64"); return simple_error!("Require one of value, value-hex, value-base64");
}; };
let data = do_inner_request(sub_arg_matches, "write", &body).await?; let data = do_inner_request(sub_arg_matches, "write", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?); println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0)) Ok(Some(0))
} }
@@ -183,7 +273,7 @@ async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
return simple_error!("Require one of value, value-hex, value-base64"); return simple_error!("Require one of value, value-hex, value-base64");
}; };
let data = do_inner_request(sub_arg_matches, "encrypt", &body).await?; let data = do_inner_request(sub_arg_matches, "encrypt", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?); println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0)) Ok(Some(0))
} }
@@ -193,30 +283,59 @@ async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
let body = json!({ "encrypted_value": value }); let body = json!({ "encrypted_value": value });
let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?; let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?); println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0)) Ok(Some(0))
} }
fn do_offline_init(_arg_matches: &ArgMatches<'_>, _sub_arg_matches: &ArgMatches<'_>) -> CommandError { fn do_offline_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let line = read_line("Input master key: ")?; let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
let master_key = if line.starts_with("hex:") {
let hex: String = line.chars().skip(4).collect(); let line = read_line("Input master key: ", read_from_pinentry)?;
hex::decode(&hex)? let jwk = read_line("Input JWK: ", read_from_pinentry)?;
} else if line.starts_with("base64:") {
let base64: String = line.chars().skip(7).collect(); let encrypted_master_key = master_key_encrypt(&line, &jwk)?;
STANDARD.decode(&base64)?
} else {
line.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); success!("Encrypted master key: {}", encrypted_master_key);
Ok(Some(0)) Ok(Some(0))
} }
fn read_line(prompt: &str) -> XResult<String> { 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().write_all(prompt.as_bytes()).ok();
std::io::stdout().flush().ok(); std::io::stdout().flush().ok();
let mut line = String::new(); let mut line = String::new();
@@ -226,6 +345,11 @@ fn read_line(prompt: &str) -> XResult<String> {
Ok(line.trim().to_string()) 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> { async fn response_to_value(response: Response<Body>) -> XResult<Value> {
let req_body = response.into_body(); let req_body = response.into_body();
let whole_body = hyper::body::aggregate(req_body).await?; let whole_body = hyper::body::aggregate(req_body).await?;
@@ -241,10 +365,10 @@ async fn do_inner_request(sub_arg_matches: &ArgMatches<'_>, action: &str, body:
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?; let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK { // if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16(); // let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?; // let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data); // return simple_error!("Server status is not success: {}, response: {}", status, data);
} // }
response_to_value(req_response).await response_to_value(req_response).await
} }

148
src/db.rs
View File

@@ -1,5 +1,5 @@
use rusqlite::{Connection, params}; use rusqlite::{params, Connection};
use rust_util::{debugging, information, opt_result, simple_error, success, XResult}; use rust_util::{opt_result, simple_error, XResult};
pub const DEFAULT_MASTER_KEY_VERIFICATION_KEY: &str = "__master_verification_key"; pub const DEFAULT_MASTER_KEY_VERIFICATION_KEY: &str = "__master_verification_key";
@@ -9,47 +9,111 @@ pub struct Key {
pub comment: Option<String>, pub comment: Option<String>,
} }
pub fn make_db_key_name(name: &str) -> String { pub struct Keys {
pub count: usize,
pub keys: Vec<Key>,
}
pub fn make_value_key_name(name: &str) -> String {
format!("value:{}", name) format!("value:{}", name)
} }
pub fn make_data_key_name(name: &str) -> String {
format!("data_key:{}", name)
}
pub fn open_db(db: &str) -> XResult<Connection> { pub fn open_db(db: &str) -> XResult<Connection> {
let con = opt_result!(Connection::open(db), "Open sqlite db: {}, failed: {}", db); let con = opt_result!(Connection::open(db), "Open sqlite db: {}, failed: {}", db);
debugging!("Db auto commit: {}", con.is_autocommit()); log::debug!("Db auto commit: {}", con.is_autocommit());
Ok(con) Ok(con)
} }
pub fn init_db(conn: &Connection) -> XResult<bool> { pub fn init_db(conn: &Connection) -> XResult<bool> {
let mut stmt = conn.prepare( if let Ok(false) = check_table_keys(conn) {
"SELECT name FROM sqlite_master WHERE type='table' AND name='keys'")?; repair_table_keys(conn)?;
let mut rows = stmt.query(())?;
if rows.next()?.is_some() {
information!("Table keys exists, skip init");
return Ok(false);
} }
let _ = conn.execute(r##" Ok(true)
}
fn repair_table_keys(conn: &Connection) -> XResult<()> {
let field_names = list_table_fields(conn, "keys")?;
let field_names = field_names.iter().map(|n| n.as_str()).collect::<Vec<_>>();
if !field_names.contains(&"comment") {
log::info!("Repair table keys, add column comment");
let _ = conn.execute("ALTER TABLE keys ADD COLUMN comment TEXT", ())?;
}
Ok(())
}
fn list_table_fields(conn: &Connection, table: &str) -> XResult<Vec<String>> {
let mut stmt_query_fields = conn.prepare(&format!("pragma table_info({})", table))?;
let mut rows_query_field = stmt_query_fields.query(())?;
let mut field_names = vec![];
let mut next_query_field_opt = rows_query_field.next()?;
while let Some(next_query_field) = next_query_field_opt {
// cid|name|type|notnull|dflt_value|pk
// ^ ^ ^ ^ ^ ^
// | | | | | [5] - Is column PK
// | | | | [4] Column default value
// | | | [3] Is column not null
// | | [2] Column type
// | [1] Column name
// [0] Column index
let field_name: String = next_query_field.get(1)?;
field_names.push(field_name.to_lowercase());
next_query_field_opt = rows_query_field.next()?;
}
log::trace!("Table {} fields: {:?}", table, field_names);
Ok(field_names)
}
fn check_table_keys(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_none() {
log::info!("Create table keys");
create_table_keys(conn)?;
Ok(true)
} else {
Ok(false)
}
}
fn create_table_keys(conn: &Connection) -> XResult<()> {
let _ = conn.execute(
r##"
CREATE TABLE keys ( CREATE TABLE keys (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
value TEXT, value TEXT,
comment TEXT comment TEXT
) )"##,
"##, ())?; (),
success!("Table keys created"); )?;
Ok(true) Ok(())
} }
pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> { pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> {
let default_comment = "".to_string(); let default_comment = "".to_string();
log::debug!("insert key name={}", &key.name);
let _ = conn.execute( let _ = conn.execute(
"INSERT INTO keys (name, value, comment) VALUES (?1, ?2, ?3)", "INSERT INTO keys (name, value, comment) VALUES (?1, ?2, ?3)",
(&key.name, &key.encrypted_key, key.comment.as_ref().unwrap_or(&default_comment)), (
&key.name,
&key.encrypted_key,
key.comment.as_ref().unwrap_or(&default_comment),
),
)?; )?;
Ok(()) Ok(())
} }
pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> { pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> {
log::debug!("update key name={}", &key.name);
if let Some(comment) = &key.comment { if let Some(comment) = &key.comment {
let _ = conn.execute( let _ = conn.execute(
"UPDATE keys SET value = ?1, comment = ?2 WHERE name = ?3", "UPDATE keys SET value = ?1, comment = ?2 WHERE name = ?3",
@@ -64,6 +128,48 @@ pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> {
Ok(()) Ok(())
} }
pub fn list_keys(conn: &Connection, ty: &str, search: &str, limit: usize) -> XResult<Keys> {
let name = format!("{}:%{}%", ty, search);
let mut count_stmt = conn.prepare("SELECT count(*) FROM keys WHERE name like ?1")?;
let mut count_iter = count_stmt.query_map(params![name], |row| {
let count: usize = row.get(0)?;
Ok(count)
})?;
let count = match count_iter.next() {
None => 0,
Some(Ok(count)) => count,
Some(Err(e)) => return simple_error!("List keys failed: {}", e),
};
log::debug!("found {} keys via: {}, limit: {}", count, name, limit);
let mut keys = vec![];
if count > 0 {
let mut list_stmt =
conn.prepare("SELECT id, name, value, comment FROM keys WHERE name like ?1 LIMIT ?2")?;
let mut list_iter = list_stmt.query_map(params![name, limit], |row| {
Ok(Key {
name: row.get(1)?,
encrypted_key: row.get(2)?,
comment: row.get(3)?,
})
})?;
loop {
match list_iter.next() {
None => {
break;
}
Some(Ok(r)) => {
log::debug!("found key name={}", r.name);
keys.push(r);
}
Some(Err(e)) => return simple_error!("List keys failed: {}", e),
}
}
}
Ok(Keys { count, keys })
}
pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> { pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> {
let mut stmt = conn.prepare("SELECT id, name, value, comment FROM keys WHERE name = ?1")?; let mut stmt = conn.prepare("SELECT id, name, value, comment FROM keys WHERE name = ?1")?;
let mut key_iter = stmt.query_map(params![name], |row| { let mut key_iter = stmt.query_map(params![name], |row| {
@@ -74,8 +180,14 @@ pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> {
}) })
})?; })?;
match key_iter.next() { match key_iter.next() {
None => Ok(None), None => {
Some(Ok(r)) => Ok(Some(r)), log::debug!("key name={} not exists", name);
Ok(None)
}
Some(Ok(r)) => {
log::debug!("found key name={}", name);
Ok(Some(r))
}
Some(Err(e)) => simple_error!("Find key failed: {}", e), Some(Err(e)) => simple_error!("Find key failed: {}", e),
} }
} }

View File

@@ -7,7 +7,7 @@ use rand::{random, thread_rng};
use rsa::pkcs1::LineEnding; use rsa::pkcs1::LineEnding;
use rsa::pkcs8::EncodePublicKey; use rsa::pkcs8::EncodePublicKey;
use rsa::{Oaep, RsaPrivateKey, RsaPublicKey}; use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
use rust_util::{opt_result, simple_error, XResult}; use rust_util::{iff, opt_result, simple_error, XResult};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use sha1::Sha1; use sha1::Sha1;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -18,12 +18,17 @@ const JWE_ALG_A256KW: &str = "A256KW";
const JWE_ALG_RSA_OAEP: &str = "RSA-OAEP"; const JWE_ALG_RSA_OAEP: &str = "RSA-OAEP";
const JWE_DOT: &str = "."; const JWE_DOT: &str = ".";
#[derive(Debug, Serialize, Deserialize)] #[derive(Default, Debug, Serialize, Deserialize)]
pub struct JweHeader { pub struct JweHeader {
pub enc: String, pub enc: String,
pub alg: String, pub alg: String,
pub vendor: String, pub vendor: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>, pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exportable: Option<bool>,
} }
pub fn generate_rsa_key(bits: u32) -> XResult<RsaPrivateKey> { pub fn generate_rsa_key(bits: u32) -> XResult<RsaPrivateKey> {
@@ -45,7 +50,7 @@ pub fn rsa_key_to_pem(rsa_private_key: &RsaPrivateKey) -> XResult<String> {
} }
pub fn jwk_to_rsa_pubic_key(rsa_jwk: &str) -> XResult<RsaPublicKey> { pub fn jwk_to_rsa_pubic_key(rsa_jwk: &str) -> XResult<RsaPublicKey> {
let rsa: Rsa = opt_result!(serde_json::from_str(&rsa_jwk), "Bad RSA JWK: {}, error: {}", rsa_jwk); let rsa: Rsa = opt_result!(serde_json::from_str(rsa_jwk), "Bad RSA JWK: {}, error: {}", rsa_jwk);
let rsa_public_key = opt_result!(RsaPublicKey::try_from(rsa), "Bad RSA JWK: {}, error: {:?}", rsa_jwk); let rsa_public_key = opt_result!(RsaPublicKey::try_from(rsa), "Bad RSA JWK: {}, error: {:?}", rsa_jwk);
Ok(rsa_public_key) Ok(rsa_public_key)
} }
@@ -55,7 +60,7 @@ pub fn serialize_jwe_rsa(payload: &[u8], rsa_public_key: &RsaPublicKey) -> XResu
enc: JWE_ENC_A256GCM.to_string(), enc: JWE_ENC_A256GCM.to_string(),
alg: JWE_ALG_RSA_OAEP.to_string(), alg: JWE_ALG_RSA_OAEP.to_string(),
vendor: "local-mini-kms".to_string(), vendor: "local-mini-kms".to_string(),
version: None, ..Default::default()
}; };
serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> { serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> {
let mut r = thread_rng(); let mut r = thread_rng();
@@ -68,24 +73,30 @@ pub fn deserialize_jwe_rsa(jwe: &str, rsa: &RsaPrivateKey) -> XResult<(Vec<u8>,
if alg != JWE_ALG_RSA_OAEP { if alg != JWE_ALG_RSA_OAEP {
return simple_error!("Invalid JWE header alg: {}", alg); return simple_error!("Invalid JWE header alg: {}", alg);
} }
Ok(opt_result!(rsa.decrypt(Oaep::new::<Sha1>(), &key_wrap), "Unwrap key failed: {}")) Ok(opt_result!(rsa.decrypt(Oaep::new::<Sha1>(), key_wrap), "Unwrap key failed: {}"))
}) })
} }
pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult<String> { pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult<String> {
serialize_jwe_aes_32(payload, to_bytes32(key)?) serialize_jwe_aes_32(None, None, payload, to_bytes32(key)?)
} }
pub fn serialize_jwe_aes_32(payload: &[u8], key: [u8; 32]) -> XResult<String> { pub fn serialize_jwe_aes_with_data_type(data_type: &str, exportable: bool, payload: &[u8], key: &[u8]) -> XResult<String> {
serialize_jwe_aes_32(Some(data_type.to_string()), iff!(exportable, None, Some(false)), payload, to_bytes32(key)?)
}
pub fn serialize_jwe_aes_32(data_type: Option<String>, exportable: Option<bool>, payload: &[u8], key: [u8; 32]) -> XResult<String> {
let header = JweHeader { let header = JweHeader {
enc: JWE_ENC_A256GCM.to_string(), enc: JWE_ENC_A256GCM.to_string(),
alg: JWE_ALG_A256KW.to_string(), alg: JWE_ALG_A256KW.to_string(),
vendor: "local-mini-kms".to_string(), vendor: "local-mini-kms".to_string(),
version: Some(get_master_key_checksum(&key)), version: Some(get_master_key_checksum(&key)),
data_type,
exportable,
}; };
serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> { serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> {
let kek = Kek::from(key); let kek = Kek::from(key);
Ok(opt_result!(kek.wrap_vec(&data_key[..]), "Wrap key failed: {}")) Ok(opt_result!(kek.wrap_vec(data_key), "Wrap key failed: {}"))
}) })
} }
@@ -99,7 +110,7 @@ pub fn deserialize_jwe_aes_32(jwe: &str, key: [u8; 32]) -> XResult<(Vec<u8>, Jwe
return simple_error!("Invalid JWE header alg: {}", alg); return simple_error!("Invalid JWE header alg: {}", alg);
} }
let kek = Kek::from(key); let kek = Kek::from(key);
Ok(opt_result!(kek.unwrap_vec(&key_wrap), "Unwrap key failed: {}")) Ok(opt_result!(kek.unwrap_vec(key_wrap), "Unwrap key failed: {}"))
}) })
} }
@@ -125,7 +136,7 @@ where
LOCAL_KMS_PREFIX, LOCAL_KMS_PREFIX,
header_b64, header_b64,
URL_SAFE_NO_PAD.encode(&cek), URL_SAFE_NO_PAD.encode(&cek),
URL_SAFE_NO_PAD.encode(&iv), URL_SAFE_NO_PAD.encode(iv),
URL_SAFE_NO_PAD.encode(&ciphertext), URL_SAFE_NO_PAD.encode(&ciphertext),
URL_SAFE_NO_PAD.encode(&tag) URL_SAFE_NO_PAD.encode(&tag)
)) ))
@@ -175,9 +186,7 @@ fn to_bytes32(bytes: &[u8]) -> XResult<[u8; 32]> {
return simple_error!("Not valid 32 bytes"); return simple_error!("Not valid 32 bytes");
} }
let mut ret = [0; 32]; let mut ret = [0; 32];
for i in 0..32 { ret.copy_from_slice(&bytes[..32]);
ret[i] = bytes[i];
}
Ok(ret) Ok(ret)
} }
@@ -198,3 +207,137 @@ fn get_jwe(jwe: &str) -> String {
jwe.to_string() jwe.to_string()
} }
} }
#[test]
fn test_jwe_rsa_01() {
let rsa_private_key = RsaPrivateKey::new(&mut thread_rng(), 2048).unwrap();
let jwe = serialize_jwe_rsa(b"hello world", &rsa_private_key.to_public_key()).unwrap();
let plaintext = deserialize_jwe_rsa(&jwe, &rsa_private_key).unwrap();
assert_eq!("hello world", String::from_utf8(plaintext.0).unwrap());
assert_eq!("A256GCM", plaintext.1.enc);
assert_eq!("RSA-OAEP", plaintext.1.alg);
assert_eq!("local-mini-kms", plaintext.1.vendor);
assert!(plaintext.1.version.is_none());
assert!(plaintext.1.data_type.is_none());
assert!(plaintext.1.exportable.is_none());
}
#[test]
fn test_jwe_rsa_02() {
let test_private_key_pem = "-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCxytLReN50hqYH
a5Db87E9QEIg52Q+H0PGSC7ZLmBSeITLUX3fm5QOtUNzGR2cNYiS9XM4G0vjvE6u
d3k4xd/YU43fuD+bLGIxXItm0uXF7hDkdgX/bGrbuuyf1aD6I8RI/vb+n3pB0TL6
q1aUlxj7lHeUdYYgAl7qsbPvfXkSEWZ712/D6oQzvEZHq7sEtPiHcqckkqlwyP5X
0Uu7INm5c9BzYNF7Ni0+xHLMGmoKh3cWrM5FI90fXpY5A/3ylVGaW6DYBh8/Fc5e
1gGpUW+LSHufAuIn6YBiPcayGIlcjYkJ6lskNvt94G2ArZq0E5ZiPM8oLSnXJTKs
kjHWJxZVAgMBAAECggEAO6cmxvu7//LxoAm6R+Ji9H8r8OhSXPmWft/XQC1sSh9/
xswn5K/JpImzANpLcg8QLObH6upVsyqKZ8VUWfUiXu3h609hoAnrRE6dwzk9uQRg
jJcA+iuBSwpTvGksIuF/SVKqwtH1bkHnze1RFnf//OFaoegwwWqqOCq5icBar/Gx
PJ6rMvPhnwpHcJENUIBOUJ22GyOBm++NeAT/Ad1eWJUJATO+wv/cp/WAV6sER32r
HXoep0GLEqD3fE9Gg1+af3u+klli6fffd0p6Whd5f4qRIUDMSn1Pid2Rm+iZMrfa
sZ2d341WRuauyT+fltIWOMdORQnoIEVkWkSOwVZo3QKBgQDJIh0wjuBikZj6yvr3
xUV7LVXtttdyll5y+HWcfWZkIHdHM1YdgmSs43Xt3H2x3TIthoGd5+lQyBCT7HAD
noHnQasdOr3XgKrU8aWRZfmJoqZud2JaGcgSoIJYYm4Dg/cJZZs1khYpAfAy2m2G
90A4dPS2m7yR9qIKU0Qzfi0jvwKBgQDiSrjLMn0vNYR3cyNepyMJnjIFaEOVBYxx
FuRLKqF8A1VY955icdffqWOWj26SCAg+dG8tgekzT626zR9HVXuyKoOHL/xqrYoH
1m9qTqlc/BwQuCpnTWkWB+X6QZ+LAvEI+q3NvX4u0MaEz5k8HoHh/FokBhZCd8Xx
CnEJRuI66wKBgQC5iJRwhHuLsU7ymWbkQ78SHwHS5ATdmMPLPlIPZsWauzAMJ9ja
I7wGl1PjdK8l8SsmP5s1NAZPFB3mtgWl8QNXdYYI0nToY7Ix+C6Ibw6+3aC06b95
6apbNGDIcxOUfpjUvDtuMTHr5fPSlRbPlyhQa+KIyCQsaNuUtraE4XF2NQKBgAjr
NwNTNxngOxtWH2PApKHhaUlLRbae5F0ksNTBlmHD495AzC5HHGIFVBaACrSYunJz
tloNz7ok/szo/r2aAekQweRXINS3iEQs0HFZLlWq3hROneU6aNTkdMz7PyWEdSmM
b8M3H70Kn3hmkjyLW0Uj5A2da80s2VFZvwDFmW8HAoGAQWxSgNONrWfhgiACpE4P
SdpE/AHD77rMkahIxb5m1KcRk4Zsk31+XZ2QtmkoYsYuH+D0z09KCia0vNUk/Pkd
8OLAbOHe+WdXxdtVq+xWUyNGmiV4ChFTmSUgYB16dLHS771pgtr97DvbZx0QC+4o
Ay9yo20HQ4fizpfOIkoW96Q=
-----END PRIVATE KEY-----";
let jwe = "LKMS:eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAiLCJ2ZW5kb3IiOiJsb2NhbC1taW5p\
LWttcyJ9.rei0aIWvri7nsy-UHJ163r1REqK897DHNVev6OCdQK7h0mH3dmHzaXJZ_CXiF3muylv8WU7nmhHLZsMD_z63rLrmH\
_pqH7grS2jTHEfHWgzkjSevFCOQxzSDtFiWJAlXQ_lA2N7y7FvJPM_W_OMZe9dFC2xDlgqXobgDHtrXrUVcjpd2wr2jCqP7RbE\
P8fnpIHRbBasC6kZggKRuyfd-YAbCNEjjaMgsZt-v2ridJVEccKFH37fu26m-5zWUbLHnl6afLf105Tkc47BbsLZXuz9gKomBa\
8cj6g6XjuZx8p8cOmlr5wnQElvFr_9prNFGFhB9c5mEsnGYbDrkM8ghCg.QmDRJCCzghCQlM7s.UF9RqEH9ULMb2is.wPGBxLP\
WkEcrkjsepiZPDA";
use rsa::pkcs8::DecodePrivateKey;
use serde_json::Value;
let rsa_private_key = RsaPrivateKey::from_pkcs8_pem(test_private_key_pem).unwrap();
let plaintext = deserialize_jwe_rsa(&jwe, &rsa_private_key).unwrap();
assert_eq!("hello world", String::from_utf8(plaintext.0).unwrap());
assert_eq!("A256GCM", plaintext.1.enc);
assert_eq!("RSA-OAEP", plaintext.1.alg);
assert_eq!("local-mini-kms", plaintext.1.vendor);
assert!(plaintext.1.version.is_none());
assert!(plaintext.1.data_type.is_none());
assert!(plaintext.1.exportable.is_none());
let public_key_pem = rsa_key_to_pem(&rsa_private_key).unwrap();
assert_eq!("-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAscrS0XjedIamB2uQ2/Ox
PUBCIOdkPh9Dxkgu2S5gUniEy1F935uUDrVDcxkdnDWIkvVzOBtL47xOrnd5OMXf
2FON37g/myxiMVyLZtLlxe4Q5HYF/2xq27rsn9Wg+iPESP72/p96QdEy+qtWlJcY
+5R3lHWGIAJe6rGz7315EhFme9dvw+qEM7xGR6u7BLT4h3KnJJKpcMj+V9FLuyDZ
uXPQc2DRezYtPsRyzBpqCod3FqzORSPdH16WOQP98pVRmlug2AYfPxXOXtYBqVFv
i0h7nwLiJ+mAYj3GshiJXI2JCepbJDb7feBtgK2atBOWYjzPKC0p1yUyrJIx1icW
VQIDAQAB
-----END PUBLIC KEY-----
", public_key_pem);
let jwk = rsa_key_to_jwk(&rsa_private_key).unwrap();
let jwk_value: Value = serde_json::from_str(&serde_json::to_string(&jwk.key).unwrap()).unwrap();
assert_eq!("AQAB", jwk_value.get("e").unwrap().as_str().unwrap());
assert_eq!("RSA", jwk_value.get("kty").unwrap().as_str().unwrap());
assert_eq!("scrS0XjedIamB2uQ2_OxPUBCIOdkPh9Dxkgu2S5gUniEy1F935uUDrVDcxkdnDWIkvVzOBtL47xOrnd5O\
MXf2FON37g_myxiMVyLZtLlxe4Q5HYF_2xq27rsn9Wg-iPESP72_p96QdEy-qtWlJcY-5R3lHWGIAJe6rGz7315EhFme9dvw-\
qEM7xGR6u7BLT4h3KnJJKpcMj-V9FLuyDZuXPQc2DRezYtPsRyzBpqCod3FqzORSPdH16WOQP98pVRmlug2AYfPxXOXtYBqVF\
vi0h7nwLiJ-mAYj3GshiJXI2JCepbJDb7feBtgK2atBOWYjzPKC0p1yUyrJIx1icWVQ",
jwk_value.get("n").unwrap().as_str().unwrap());
let rsa_public_key_2 = jwk_to_rsa_pubic_key(&serde_json::to_string(&jwk.key).unwrap()).unwrap();
let jwe = serialize_jwe_rsa(b"hello world 2", &rsa_public_key_2).unwrap();
let plaintext = deserialize_jwe_rsa(&jwe, &rsa_private_key).unwrap();
assert_eq!("hello world 2", String::from_utf8(plaintext.0).unwrap());
assert_eq!("A256GCM", plaintext.1.enc);
assert_eq!("RSA-OAEP", plaintext.1.alg);
assert_eq!("local-mini-kms", plaintext.1.vendor);
assert!(plaintext.1.version.is_none());
assert!(plaintext.1.data_type.is_none());
assert!(plaintext.1.exportable.is_none());
}
#[test]
fn test_jwe_aes() {
let jwe = serialize_jwe_aes(b"hello world", b"01234567890123456789012345678901").unwrap();
let plaintext = deserialize_jwe_aes(&jwe, b"01234567890123456789012345678901").unwrap();
assert_eq!("hello world", String::from_utf8(plaintext.0).unwrap());
assert_eq!("A256GCM", plaintext.1.enc);
assert_eq!("A256KW", plaintext.1.alg);
assert_eq!("local-mini-kms", plaintext.1.vendor);
assert_eq!("a15a8f066b1af95d", plaintext.1.version.unwrap());
assert!(plaintext.1.data_type.is_none());
assert!(plaintext.1.exportable.is_none());
}
#[test]
fn test_decode_url_safe_no_pad() {
assert_eq!("68656c6c6f20776f726c64",
hex::encode(decode_url_safe_no_pad(&URL_SAFE_NO_PAD.encode("hello world")).unwrap()));
}
#[test]
fn test_to_bytes32() {
assert!(to_bytes32(b"").is_err());
assert!(to_bytes32(b"01234567890123456789012345678901").is_ok());
assert_eq!("3031323334353637383930313233343536373839303132333435363738393031",
hex::encode(to_bytes32(b"01234567890123456789012345678901").unwrap()));
}
#[test]
fn test_get_master_key_checksum() {
assert_eq!("685fb69d15d5efe9", get_master_key_checksum(b"test"));
assert_eq!("80c5603e00ca3e80", get_master_key_checksum(b"hello_world"));
}
#[test]
fn test_get_jwe() {
assert_eq!("test", get_jwe("test"));
assert_eq!("test", get_jwe("LKMS:test"));
}

View File

@@ -2,6 +2,11 @@ use clap::{App, AppSettings, ArgMatches};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::{failure_and_exit, information, success, warning}; use rust_util::{failure_and_exit, information, success, warning};
use zeroizing_alloc::ZeroAlloc;
#[global_allocator]
static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
mod db; mod db;
mod proc; mod proc;
mod jose; mod jose;
@@ -17,6 +22,7 @@ mod yubikey_hmac;
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
mod yubikey_init_master_key; mod yubikey_init_master_key;
mod serve_datakey; mod serve_datakey;
mod serve_log;
pub struct DefaultCommandImpl; pub struct DefaultCommandImpl;
@@ -36,6 +42,7 @@ fn main() {
} }
} }
#[cfg(feature = "harden_process")]
fn harden_process() { fn harden_process() {
let ignore_harden_process_error = std::env::var("IGNORE_HARDEN_PROCESS_ERROR") let ignore_harden_process_error = std::env::var("IGNORE_HARDEN_PROCESS_ERROR")
.map(|v| &v == "true").unwrap_or_else(|_| false); .map(|v| &v == "true").unwrap_or_else(|_| false);
@@ -61,6 +68,8 @@ fn inner_main() -> CommandError {
{ {
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
features.push("yubikey".to_string()); features.push("yubikey".to_string());
#[cfg(feature = "harden_process")]
features.push("harden_process".to_string());
} }
let long_about = format!("Local mini KMS, features: [{}]", features.join(", ")); let long_about = format!("Local mini KMS, features: [{}]", features.join(", "));
let mut app = App::new(env!("CARGO_PKG_NAME")) let mut app = App::new(env!("CARGO_PKG_NAME"))
@@ -75,6 +84,7 @@ fn inner_main() -> CommandError {
let matches = app.get_matches(); let matches = app.get_matches();
for command in &commands { for command in &commands {
if let Some(sub_cmd_matches) = matches.subcommand_matches(command.name()) { if let Some(sub_cmd_matches) = matches.subcommand_matches(command.name()) {
#[cfg(feature = "harden_process")]
if command.name() == "serve" { harden_process(); } if command.name() == "serve" { harden_process(); }
return command.run(&matches, sub_cmd_matches); return command.run(&matches, sub_cmd_matches);
} }

View File

@@ -7,12 +7,12 @@ pub struct Process {
pub exec: Option<PathBuf>, pub exec: Option<PathBuf>,
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(all(target_os = "linux", feature = "harden_process")))]
pub fn get_process(_port: u16) -> Option<Process> { pub fn get_process(_port: u16) -> Option<Process> {
None None
} }
#[cfg(target_os = "linux")] #[cfg(all(target_os = "linux", feature = "harden_process"))]
pub fn get_process(port: u16) -> Option<Process> { pub fn get_process(port: u16) -> Option<Process> {
let all_procs = match procfs::process::all_processes() { let all_procs = match procfs::process::all_processes() {
Err(e) => { Err(e) => {

View File

@@ -1,5 +1,3 @@
use std::net::SocketAddr;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::server::conn::AddrStream; use hyper::server::conn::AddrStream;
@@ -8,10 +6,11 @@ use hyper::{Body, Client, Method, Request, Response, Server, StatusCode};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::{failure_and_exit, information, success, warning, XResult}; use rust_util::{failure_and_exit, information, success, warning, XResult};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::serve_common::{self, GenericError, MemoryKey, Result}; use crate::serve_common::{self, GenericError, MemoryKey, Result};
use crate::serve_encrypt_decrypt;
use crate::serve_init; use crate::serve_init;
use crate::serve_init::InitRequest; use crate::serve_init::InitRequest;
use crate::serve_read_write; use crate::serve_read_write;
@@ -20,22 +19,77 @@ use crate::serve_status;
use crate::yubikey_hmac; use crate::yubikey_hmac;
use crate::{db, jose, proc}; use crate::{db, jose, proc};
use crate::{do_response, serve_datakey}; use crate::{do_response, serve_datakey};
use crate::{serve_encrypt_decrypt, serve_log};
lazy_static::lazy_static! {
pub static ref GLOBAL_REQUEST_COUNT: AtomicU64 = AtomicU64::new(0);
}
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
fn name(&self) -> &str { "serve" } fn name(&self) -> &str {
"serve"
}
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Local mini KMS serve") SubCommand::with_name(self.name())
.arg(Arg::with_name("listen").long("listen").short("L").takes_value(true).default_value("127.0.0.1:5567").help("Listen")) .about("Local mini KMS serve")
.arg(Arg::with_name("local-db").long("local-db").short("d").takes_value(true).default_value("local-mini-kms.db").help("Local db file")) .arg(
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge")) Arg::with_name("listen")
.arg(Arg::with_name("init-encrypted-master-key").long("init-encrypted-master-key").short("k").takes_value(true).help("Init encrypted mater key")) .long("listen")
.short("L")
.takes_value(true)
.default_value("127.0.0.1:5567")
.help("Listen"),
)
.arg(
Arg::with_name("local-db")
.long("local-db")
.short("d")
.takes_value(true)
.default_value("local-mini-kms.db")
.help("Local db file"),
)
.arg(
Arg::with_name("yubikey-challenge")
.long("yubikey-challenge")
.short("c")
.takes_value(true)
.help("Yubikey challenge"),
)
.arg(
Arg::with_name("init-encrypted-master-key")
.long("init-encrypted-master-key")
.short("k")
.takes_value(true)
.help("Init encrypted mater key"),
)
.arg(
Arg::with_name("log-level")
.long("log-level")
.takes_value(true)
.help("Log level: trace, debug, info, warn or error"),
)
.arg(
Arg::with_name("log-file")
.long("log-file")
.takes_value(true)
.help("Log file #DEFAULT or config file"),
)
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { 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"); let log_level = sub_arg_matches.value_of("log-level");
let log_file = sub_arg_matches.value_of("log-file");
if let Err(e) = serve_log::init_logger(log_level, log_file) {
println!("[ERROR] Init logger failed: {}", e);
}
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) { match init_instance(local_mini_kms_db) {
Ok(true) => success!("Init server success"), Ok(true) => success!("Init server success"),
Ok(false) => failure_and_exit!("SHOULD NOT HAPPEN, server already init"), Ok(false) => failure_and_exit!("SHOULD NOT HAPPEN, server already init"),
@@ -46,9 +100,13 @@ impl Command for CommandImpl {
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
init_with_yubikey_challenge(&rt, sub_arg_matches); init_with_yubikey_challenge(&rt, sub_arg_matches);
let listen = sub_arg_matches.value_of("listen").expect("Get argument listen error"); let listen = sub_arg_matches
.value_of("listen")
.expect("Get argument listen error");
rt.block_on(async { rt.block_on(async {
let addr = listen.parse().unwrap_or_else(|_| panic!("Parse listen error: {}", listen)); let addr = listen
.parse()
.unwrap_or_else(|_| panic!("Parse listen error: {}", listen));
let client = Client::new(); let client = Client::new();
let new_service = make_service_fn(move |conn: &AddrStream| { let new_service = make_service_fn(move |conn: &AddrStream| {
let remote_addr = conn.remote_addr(); let remote_addr = conn.remote_addr();
@@ -61,6 +119,7 @@ impl Command for CommandImpl {
}); });
let server = Server::bind(&addr).serve(new_service); let server = Server::bind(&addr).serve(new_service);
information!("Listening on http://{}", addr); information!("Listening on http://{}", addr);
log::info!("Listening on http://{}", addr);
match server.await { match server.await {
Err(e) => failure_and_exit!("Server error: {}", e), Err(e) => failure_and_exit!("Server error: {}", e),
Ok(_) => success!("Server ended"), Ok(_) => success!("Server ended"),
@@ -77,11 +136,19 @@ async fn response_requests(
req: Request<Body>, req: Request<Body>,
_client: Client<HttpConnector>, _client: Client<HttpConnector>,
) -> Result<Response<Body>> { ) -> Result<Response<Body>> {
let request_idx = GLOBAL_REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
let process = proc::get_process(remote_addr.port()); let process = proc::get_process(remote_addr.port());
match process { match process {
None => information!( "Receive request: {}, from: {}", req.uri(), remote_addr ), None => log::info!(
Some(process) => information!( "[{:06}] Receive request: {} {}, from: {}",
"Receive request: {}, from: {}, process: {} {} {:?}", request_idx,
req.method(),
req.uri(),
remote_addr
),
Some(process) => log::info!(
"[{:06}] Receive request: {}, from: {}, process: {} {} {:?}",
request_idx,
req.uri(), req.uri(),
remote_addr, remote_addr,
process.pid, process.pid,
@@ -94,17 +161,22 @@ async fn response_requests(
(&Method::POST, "/update") => update().await, (&Method::POST, "/update") => update().await,
(&Method::POST, "/decrypt") => serve_encrypt_decrypt::decrypt(req).await, (&Method::POST, "/decrypt") => serve_encrypt_decrypt::decrypt(req).await,
(&Method::POST, "/encrypt") => serve_encrypt_decrypt::encrypt(req).await, (&Method::POST, "/encrypt") => serve_encrypt_decrypt::encrypt(req).await,
(&Method::POST, "/list") => serve_read_write::list(req).await,
(&Method::POST, "/read") => serve_read_write::read(req).await, (&Method::POST, "/read") => serve_read_write::read(req).await,
(&Method::POST, "/write") => serve_read_write::write(req).await, (&Method::POST, "/write") => serve_read_write::write(req).await,
(&Method::POST, "/datakey") => serve_datakey::generate(req).await, (&Method::POST, "/datakey") => serve_datakey::generate(req).await,
(&Method::GET, "/status") => serve_status::status().await, (&Method::GET, "/status") => serve_status::status().await,
(&Method::GET, "/version") => get_version().await, (&Method::GET, "/version") => get_version().await,
(&Method::GET, "/") => get_root().await, (&Method::GET, "/") => get_root().await,
_ => Ok(Response::builder() _ => Ok(Response::builder().status(StatusCode::NOT_FOUND).body(
.status(StatusCode::NOT_FOUND) format!(
.body(format!("{}\n", serde_json::to_string_pretty(&json!({ "{}\n",
serde_json::to_string_pretty(&json!({
"error": "not_found", "error": "not_found",
}))?).into())?), }))?
)
.into(),
)?),
} }
} }
@@ -114,7 +186,9 @@ fn init_instance(db: &str) -> XResult<bool> {
let conn = db::open_db(db)?; let conn = db::open_db(db)?;
db::init_db(&conn)?; db::init_db(&conn)?;
let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock write startup rw lock error"); let mut startup_rw_lock = serve_common::STATUP_RW_LOCK
.lock()
.expect("Lock write startup rw lock error");
match &*startup_rw_lock { match &*startup_rw_lock {
Some(_) => Ok(false), Some(_) => Ok(false),
None => { None => {
@@ -130,7 +204,9 @@ fn init_instance(db: &str) -> XResult<bool> {
} }
fn update_instance_rsa_key_pair() -> XResult<bool> { fn update_instance_rsa_key_pair() -> XResult<bool> {
let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock write startup rw lock error"); let mut startup_rw_lock = serve_common::STATUP_RW_LOCK
.lock()
.expect("Lock write startup rw lock error");
match &mut *startup_rw_lock { match &mut *startup_rw_lock {
Some(k) => { Some(k) => {
k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?; k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?;
@@ -146,20 +222,29 @@ async fn update() -> Result<Response<Body>> {
async fn inner_update() -> XResult<(StatusCode, Value)> { async fn inner_update() -> XResult<(StatusCode, Value)> {
let update = update_instance_rsa_key_pair()?; let update = update_instance_rsa_key_pair()?;
Ok((StatusCode::OK, json!({ Ok((
StatusCode::OK,
json!({
"update": update, "update": update,
}))) }),
))
} }
async fn get_version() -> Result<Response<Body>> { async fn get_version() -> Result<Response<Body>> {
Ok(Response::builder().body(format!( Ok(Response::builder().body(
"{} - {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") format!(
).into())?) "{} - {}\n",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
)
.into(),
)?)
} }
async fn get_root() -> Result<Response<Body>> { async fn get_root() -> Result<Response<Body>> {
if std::env::var("LOCAL_MINI_KMS_HELP").is_ok() { if std::env::var("LOCAL_MINI_KMS_HELP").is_ok() {
Ok(Response::builder().body(format!( Ok(Response::builder().body(
format!(
r##"{} - {} r##"{} - {}
Supports commands: Supports commands:
- GET /version - GET /version
@@ -170,8 +255,12 @@ Supports commands:
- POST /decrypt - POST /decrypt
- POST /read - POST /read
- POST /write - POST /write
"##, env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") "##,
).into())?) env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
)
.into(),
)?)
} else { } else {
Ok(Response::builder().body("Root Not Found\n".into())?) Ok(Response::builder().body("Root Not Found\n".into())?)
} }
@@ -179,14 +268,20 @@ Supports commands:
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
fn init_with_yubikey_challenge(rt: &Runtime, sub_arg_matches: &ArgMatches) { fn init_with_yubikey_challenge(rt: &Runtime, sub_arg_matches: &ArgMatches) {
let mut yubikey_challenge = sub_arg_matches.value_of("yubikey-challenge").map(ToString::to_string); let mut yubikey_challenge = sub_arg_matches
.value_of("yubikey-challenge")
.map(ToString::to_string);
let init_encrypted_master_key = sub_arg_matches.value_of("init-encrypted-master-key"); let init_encrypted_master_key = sub_arg_matches.value_of("init-encrypted-master-key");
if init_encrypted_master_key.is_some() && yubikey_challenge.is_none() { if init_encrypted_master_key.is_some() && yubikey_challenge.is_none() {
yubikey_challenge = rpassword::prompt_password("Yubikey challenge: ").ok(); yubikey_challenge =
pinentry_util::read_pin(Some("Input yubikey challenge"), Some("Challenge: "))
.ok()
.map(|p| p.get_pin().to_string());
} }
let (challenge_key, init_encrypted_master_key) = match (yubikey_challenge, init_encrypted_master_key) { let (challenge_key, init_encrypted_master_key) =
match (yubikey_challenge, init_encrypted_master_key) {
(Some(yubikey_challenge), Some(init_encrypted_master_key)) => { (Some(yubikey_challenge), Some(init_encrypted_master_key)) => {
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) { match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
Err(e) => { Err(e) => {
@@ -197,7 +292,9 @@ fn init_with_yubikey_challenge(rt: &Runtime, sub_arg_matches: &ArgMatches) {
} }
} }
(Some(_), None) | (None, Some(_)) => { (Some(_), None) | (None, Some(_)) => {
warning!("Arguments yubikey-challenge and init-encrypted-master-key should both assigned."); warning!(
"Arguments yubikey-challenge and init-encrypted-master-key should both assigned."
);
return; return;
} }
_ => return, _ => return,
@@ -213,7 +310,8 @@ fn init_with_yubikey_challenge(rt: &Runtime, sub_arg_matches: &ArgMatches) {
clear_master_key_hex: Some(hex::encode(&key)), clear_master_key_hex: Some(hex::encode(&key)),
clear_master_key_base64: None, clear_master_key_base64: None,
encrypted_master_key: None, encrypted_master_key: None,
}).await })
.await
}); });
match init_master_key_result { match init_master_key_result {
Err(e) => warning!("Init master key failed: {}", e), Err(e) => warning!("Init master key failed: {}", e),

View File

@@ -1,20 +1,41 @@
use std::io::{Cursor, Read};
use std::sync::Mutex; use std::sync::Mutex;
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
use hyper::StatusCode; use hyper::body::Buf;
use hyper::{Body, Request, StatusCode};
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
use rusqlite::Connection; use rusqlite::Connection;
use rust_util::{opt_result, simple_error, XResult}; use rust_util::{opt_result, simple_error, XResult};
use seckey::SecBytes; use seckey::SecBytes;
use serde::{Deserialize, Serialize}; use serde::{de, Deserialize, Serialize};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
use zeroize::Zeroize;
use crate::db; use crate::db;
pub type GenericError = Box<dyn std::error::Error + Send + Sync>; pub type GenericError = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, GenericError>; pub type Result<T> = std::result::Result<T, GenericError>;
pub async fn parse_request<T>(req: Request<Body>) -> XResult<T>
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::<u8>::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_export]
macro_rules! do_response { macro_rules! do_response {
($ex: expr) => ( ($ex: expr) => (
@@ -34,11 +55,12 @@ pub fn ok(body: Value) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::OK, body)) Ok((StatusCode::OK, body))
} }
pub fn error(error: &str) -> XResult<(StatusCode, Value)> { pub fn client_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok(( Ok((StatusCode::BAD_REQUEST, json!({ "error": error })))
StatusCode::BAD_REQUEST, }
json!({ "error": error })
)) pub fn server_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": error })))
} }
// pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> { // pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> {
@@ -97,8 +119,20 @@ impl MultipleViewValue {
} }
} }
#[macro_export]
macro_rules! require_master_key {
() => {
match $crate::serve_common::get_master_key() {
None => return $crate::serve_common::client_error("status_not_ready"),
Some(key) => key,
}
};
}
pub fn get_master_key() -> Option<SecBytes> { pub fn get_master_key() -> Option<SecBytes> {
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 { match &*startup_rw_lock {
None => None, None => None,
Some(k) => match &k.master_key { Some(k) => match &k.master_key {
@@ -131,11 +165,14 @@ pub fn byte_to_multi_view_map(bytes: &[u8], with_value: bool) -> Map<String, Val
} }
pub fn open_local_db() -> XResult<Connection> { pub fn open_local_db() -> XResult<Connection> {
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 { match &*startup_rw_lock {
None => simple_error!("Db is not initted!"), None => simple_error!("Db is not initiated!"),
Some(k) => { Some(k) => Ok(opt_result!(
Ok(opt_result!(db::open_db(& k.database_file), "Open db failed: {}")) db::open_db(&k.database_file),
} "Open db failed: {}"
)),
} }
} }

View File

@@ -1,24 +1,26 @@
use crate::serve_common::{get_master_key, Result}; use crate::db::Key;
use crate::{do_response, jose, serve_common}; 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::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
use hyper::body::Buf;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use rand::random; use rand::random;
use rust_util::{debugging, iff, XResult}; use rust_util::{iff, XResult};
use seckey::SecBytes; use seckey::SecBytes;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
// type:aes, spec:128,192,256 // type:aes, spec:128,192,256
// type:rsa, spec:2048,3072,4096 // type:rsa, spec:2048,3072,4096
// type:ec, spec:p256,p384,p521,ed25519,cv25519 // type:ec, spec:p256,p384,p521,ed25519,cv25519
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct DataKeyRequest { struct DataKeyRequest {
key_type: String, r#type: String,
key_spec: String, spec: String,
name: Option<String>,
comment: Option<String>,
exportable: Option<bool>,
return_plaintext: Option<bool>, return_plaintext: Option<bool>,
} }
@@ -27,51 +29,60 @@ pub async fn generate(req: Request<Body>) -> Result<Response<Body>> {
} }
async fn inner_generate(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_generate(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let request: DataKeyRequest = parse_request(req).await?;
let request: DataKeyRequest = serde_json::from_reader(whole_body.reader())?;
debugging!("Generate data key: {} {}", &request.key_type, &request.key_spec); log::debug!("Generate data key: {} {}", &request.r#type, &request.spec);
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"), let exportable = request.exportable.unwrap_or(true);
Some(key) => key, let ret_key_plaintext = iff!(!exportable, false, request.return_plaintext.unwrap_or(false));
};
let ret_key_plaintext = request.return_plaintext.unwrap_or(false);
let response_result = match (request.key_type.as_str(), request.key_spec.as_str()) { let response_result = match (request.r#type.as_str(), request.spec.as_str()) {
("aes", "128") => generate_aes("datakey.aes_128:", key, 16, ret_key_plaintext), // ("aes", "128") => generate_aes("datakey:aes-128", exportable, key, 16, ret_key_plaintext),
("aes", "192") => generate_aes("datakey.aes_192:", key, 24, ret_key_plaintext), // ("aes", "192") => generate_aes("datakey:aes-192", exportable, key, 24, ret_key_plaintext),
("aes", "256") => generate_aes("datakey.aes_256:", key, 32, ret_key_plaintext), ("aes", "256") => generate_aes("datakey:aes-256", exportable, key, 32, ret_key_plaintext),
// TODO rsa 2048, rsa 3072, rsa 4096 // TODO rsa 2048, rsa 3072, rsa 4096
// TODO ec p256, p384, p521, ed25519, cv25519 // TODO ec p256, p384, p521, ed25519, cv25519
_ => return serve_common::error("invalid key_type or key_spec"), _ => return serve_common::client_error("invalid key_type or key_spec"),
}; };
match response_result { match response_result {
Err(e) => serve_common::error(&format!("internal error: {}", e)), Err(e) => serve_common::server_error(&format!("internal error: {}", e)),
Ok((key_plaintext, key_ciphertext)) => { Ok((key_plaintext, key_ciphertext)) => {
let mut map = Map::new(); let mut map = Map::new();
map.insert("key_type".to_string(), Value::String(request.key_type)); map.insert("key_type".to_string(), Value::String(request.r#type));
map.insert("key_spec".to_string(), Value::String(request.key_spec)); map.insert("key_spec".to_string(), Value::String(request.spec));
if let Some(key_plaintext) = key_plaintext { if let Some(key_plaintext) = key_plaintext {
map.insert("key_plaintext".to_string(), Value::String(STANDARD.encode(&key_plaintext))); map.insert("key_plaintext".to_string(), Value::String(STANDARD.encode(&key_plaintext)));
} }
map.insert("key_ciphertext".to_string(), Value::String(key_ciphertext)); map.insert("key_ciphertext".to_string(), Value::String(key_ciphertext.clone()));
if let Some(name) = &request.name {
if name.is_empty() {
return serve_common::client_error("name_is_empty");
}
let conn = open_local_db()?;
let db_key_name = db::make_data_key_name(name);
let db_key = db::find_key(&conn, &db_key_name)?;
if db_key.is_some() {
return serve_common::client_error("name_exists");
}
let key = Key {
name: db_key_name,
encrypted_key: key_ciphertext,
comment: request.comment,
};
db::insert_key(&conn, &key)?;
}
Ok((StatusCode::OK, Value::Object(map))) Ok((StatusCode::OK, Value::Object(map)))
} }
} }
} }
fn generate_aes(prefix: &str, key: SecBytes, len: i32, ret_key_plaintext: bool) -> XResult<(Option<Vec<u8>>, String)> { fn generate_aes(data_key_type: &str, exportable: bool, key: SecBytes, len: i32, ret_key_plaintext: bool) -> XResult<(Option<Vec<u8>>, String)> {
let bytes: [u8; 32] = random(); let bytes: [u8; 32] = random();
let value = &bytes[0..len as usize]; let value = &bytes[0..len as usize];
let key_plaintext = iff!(ret_key_plaintext, Some(value.to_vec()), None); let key_plaintext = iff!(ret_key_plaintext, Some(value.to_vec()), None);
let key_ciphertext = jose::serialize_jwe_aes(&join_prefix_value(prefix, value), &key.read())?; let key_ciphertext = jose::serialize_jwe_aes_with_data_type(data_key_type, exportable, value, &key.read())?;
Ok((key_plaintext, key_ciphertext)) Ok((key_plaintext, key_ciphertext))
} }
fn join_prefix_value(prefix: &str, value: &[u8]) -> Vec<u8> {
let mut ret = Vec::with_capacity(prefix.len() + value.len());
ret.extend_from_slice(prefix.as_bytes());
ret.extend_from_slice(value);
ret
}

View File

@@ -1,12 +1,11 @@
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use hyper::body::Buf; use rust_util::XResult;
use rust_util::{debugging, XResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Map, Value};
use crate::do_response;
use crate::jose; use crate::jose;
use crate::serve_common::{self, byte_to_multi_view_map, get_master_key, 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)] #[derive(Serialize, Deserialize)]
struct DecryptRequest { struct DecryptRequest {
@@ -18,21 +17,32 @@ pub async fn decrypt(req: Request<Body>) -> Result<Response<Body>> {
} }
async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let data: DecryptRequest = parse_request(req).await?;
let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?;
debugging!("To be decrypted value: {}", &data.encrypted_value); log::trace!("To be decrypted value: {}", &data.encrypted_value);
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key.read()); let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key.read());
drop(key); drop(key);
let (data, header) = decrypted_value?;
decrypted_value.map(|v| { if let Some(false) = header.exportable {
let map = byte_to_multi_view_map(&v.0, true); return serve_common::client_error("data_not_exportable");
(StatusCode::OK, Value::Object(map)) }
})
let mut map = byte_to_multi_view_map(&data, true);
let mut header_map = Map::new();
header_map.insert("enc".to_string(), Value::String(header.enc.clone()));
header_map.insert("alg".to_string(), Value::String(header.alg.clone()));
if let Some(version) = &header.version {
header_map.insert("version".to_string(), Value::String(version.to_string()));
}
if let Some(data_type) = &header.data_type {
header_map.insert("data_type".to_string(), Value::String(data_type.to_string()));
}
if !header_map.is_empty() {
map.insert("header".to_string(), Value::Object(header_map));
}
Ok((StatusCode::OK, Value::Object(map)))
} }
pub async fn encrypt(req: Request<Body>) -> Result<Response<Body>> { pub async fn encrypt(req: Request<Body>) -> Result<Response<Body>> {
@@ -40,13 +50,9 @@ pub async fn encrypt(req: Request<Body>) -> Result<Response<Body>> {
} }
async fn inner_encrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_encrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let data: MultipleViewValue = parse_request(req).await?;
let data: MultipleViewValue = serde_json::from_reader(whole_body.reader())?;
let value = data.to_bytes()?; let value = data.to_bytes()?;
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let encrypt_result = jose::serialize_jwe_aes(&value, &key.read()); let encrypt_result = jose::serialize_jwe_aes(&value, &key.read());
drop(key); drop(key);

View File

@@ -1,8 +1,7 @@
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
use hyper::body::Buf;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use rust_util::{debugging, information, opt_result, success, warning, XResult}; use rust_util::{opt_result, XResult};
use seckey::SecBytes; use seckey::SecBytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
@@ -10,7 +9,7 @@ use zeroize::Zeroize;
use crate::db::Key; use crate::db::Key;
use crate::do_response; use crate::do_response;
use crate::serve_common::{self, Result}; use crate::serve_common::{self, parse_request, Result};
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
use crate::yubikey_hmac; use crate::yubikey_hmac;
use crate::{db, jose}; use crate::{db, jose};
@@ -28,18 +27,16 @@ pub struct InitRequest {
} }
async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let init_request: InitRequest = parse_request(req).await?;
let init_request: InitRequest = serde_json::from_reader(whole_body.reader())?;
inner_init_request(init_request).await inner_init_request(init_request).await
} }
pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCode, Value)> { pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCode, Value)> {
let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error"); let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
match &*startup_rw_lock { match &*startup_rw_lock {
None => return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))), None => return serve_common::server_error("instant_key_pair_not_initialized"),
Some(memory_key) => if memory_key.master_key.is_some() { Some(memory_key) => if memory_key.master_key.is_some() {
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "already init " }))); return serve_common::client_error("already_initialized");
}, },
} }
@@ -48,19 +45,19 @@ pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCod
} else if let Some(clear_master_key_hex) = init_request.clear_master_key_hex { } else if let Some(clear_master_key_hex) = init_request.clear_master_key_hex {
hex::decode(clear_master_key_hex)? hex::decode(clear_master_key_hex)?
} else if let Some(encrypted_master_key) = init_request.encrypted_master_key { } else if let Some(encrypted_master_key) = init_request.encrypted_master_key {
debugging!("Received encrypted master key: {}", encrypted_master_key); log::debug!("Received encrypted master key: {}", encrypted_master_key);
if let Some(k) = &*startup_rw_lock { if let Some(k) = &*startup_rw_lock {
let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair)?; let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair)?;
clear_master_key clear_master_key
} else { } else {
return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))); return serve_common::server_error("instant_key_pair_not_initialized");
} }
} else { } else {
return serve_common::error("master_key_missing"); return serve_common::client_error("master_key_missing");
}; };
if clear_master_key.len() != 32 { if clear_master_key.len() != 32 {
return serve_common::error("bad_master_key_length"); return serve_common::client_error("bad_master_key_length");
} }
if let Some(k) = &mut *startup_rw_lock { if let Some(k) = &mut *startup_rw_lock {
@@ -71,24 +68,24 @@ pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCod
let key = Key { let key = Key {
name: db::DEFAULT_MASTER_KEY_VERIFICATION_KEY.to_string(), 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)?, encrypted_key: jose::serialize_jwe_aes("LOCAL-MINI-KMS:MAGIC-VERIFICATION-KEY".as_bytes(), &clear_master_key)?,
comment: None, comment: Some("local-mini-kms re-init verification".to_string()),
}; };
db::insert_key(&conn, &key)?; db::insert_key(&conn, &key)?;
} }
Some(key) => { Some(key) => {
debugging!("Found jwe: {}", &key.encrypted_key); log::trace!("Found jwe: {}", &key.encrypted_key);
let _ = opt_result!(jose::deserialize_jwe_aes(&key.encrypted_key, &clear_master_key), "Deserialize master key verification key failed: {}"); let _ = opt_result!(jose::deserialize_jwe_aes(&key.encrypted_key, &clear_master_key), "Deserialize master key verification key failed: {}");
} }
} }
information!("Set master key success"); log::info!("Set master key success");
#[cfg(feature = "yubikey")] #[cfg(feature = "yubikey")]
if let Some(yubikey_challenge) = &init_request.yubikey_challenge { if let Some(yubikey_challenge) = &init_request.yubikey_challenge {
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) { match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
Err(e) => warning!("Yubikey challenge failed: {}", e), Err(e) => log::warn!("Yubikey challenge failed: {}", e),
Ok(challenge_key) => match jose::serialize_jwe_aes(&clear_master_key, &challenge_key) { Ok(challenge_key) => match jose::serialize_jwe_aes(&clear_master_key, &challenge_key) {
Err(e) => warning!("Yubikey seal master key failed: {}", e), Err(e) => log::warn!("Yubikey seal master key failed: {}", e),
Ok(jwe) => success!("Yubikey sealed master key: {}", jwe) Ok(jwe) => log::info!("Yubikey sealed master key: {}", jwe)
}, },
} }
} }

66
src/serve_log.rs Normal file
View File

@@ -0,0 +1,66 @@
use log::LevelFilter;
use rust_util::{simple_error, XResult};
use std::{env, fs};
const DEFAULT_LOG4RS_CONFIG: &str = r##"refresh_rate: 10 seconds
appenders:
file:
kind: rolling_file
path: "log/local_mini_kms_log.log"
append: true
encoder:
pattern: "{d} - {m}{n}"
policy:
kind: compound
trigger:
kind: size
limit: 100 mb
roller:
kind: fixed_window
pattern: log/local_mini_kms_log.{}.log
count: 10
root:
level: info
appenders:
- file"##;
pub fn init_logger(log_level: Option<&str>, log4rs_file: Option<&str>) -> XResult<()> {
match log4rs_file {
None => {
let level = match env::var("LOGGER").ok().as_deref().map(get_log_level_filter) {
Some(Ok(level)) => level,
_ => log_level.map(get_log_level_filter).unwrap_or(Ok(LevelFilter::Debug))?,
};
let _ = env_logger::builder().filter_level(level).try_init();
}
Some(log4rs_file) => {
let log4rs_config_file = if log4rs_file == "#DEFAULT" {
let default_log4rs_file = "default_log4rs.yaml";
let _ = fs::metadata(default_log4rs_file).map_err(|_| {
fs::write(default_log4rs_file, DEFAULT_LOG4RS_CONFIG).map_err(|e| {
println!("Write file: {} failed: {}", default_log4rs_file, e);
})
});
default_log4rs_file
} else {
log4rs_file
};
log4rs::init_file(log4rs_config_file, Default::default())?;
}
}
Ok(())
}
fn get_log_level_filter(log_level: &str) -> XResult<LevelFilter> {
Ok(match log_level {
"trace" => LevelFilter::Trace,
"debug" => LevelFilter::Debug,
"info" => LevelFilter::Info,
"warn" => LevelFilter::Warn,
"error" => LevelFilter::Error,
_ => {
return simple_error!("invalid log_level");
}
})
}

View File

@@ -1,12 +1,18 @@
use crate::db::Key; use crate::db::Key;
use crate::do_response; use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, parse_request, MultipleViewValue, Result};
use crate::serve_common::{self, byte_to_multi_view_map, get_master_key, open_local_db, MultipleViewValue, Result};
use crate::{db, jose}; use crate::{db, jose};
use hyper::body::Buf; use crate::{do_response, require_master_key};
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use rust_util::XResult; use rust_util::XResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Map, Value};
#[derive(Serialize, Deserialize)]
struct KeysQuery {
r#type: Option<String>,
name: Option<String>,
limit: Option<usize>,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct Named { struct Named {
@@ -21,31 +27,54 @@ struct NamedValue {
comment: Option<String>, comment: Option<String>,
} }
pub async fn list(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_list(req).await)
}
async fn inner_list(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let keys_query: KeysQuery = parse_request(req).await?;
let conn = open_local_db()?;
let keys = db::list_keys(
&conn,
keys_query.r#type.as_deref().unwrap_or("%"),
keys_query.name.as_deref().unwrap_or(""),
keys_query.limit.unwrap_or(10),
)?;
let mut map = Map::new();
map.insert("count".to_string(), keys.count.into());
let keys = Value::Array(
keys.keys
.iter()
.map(|k| k.name.to_string().into())
.collect(),
);
map.insert("keys".to_string(), keys);
serve_common::ok(Value::Object(map))
}
pub async fn read(req: Request<Body>) -> Result<Response<Body>> { pub async fn read(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_read(req).await) do_response!(inner_read(req).await)
} }
async fn inner_read(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_read(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let named: Named = parse_request(req).await?;
let named: Named = serde_json::from_reader(whole_body.reader())?;
let name = &named.name; let name = &named.name;
if name.is_empty() { if name.is_empty() {
return serve_common::error("key_name_is_empty"); return serve_common::client_error("name_is_empty");
} }
let db_key_name = db::make_db_key_name(name); let db_key_name = db::make_value_key_name(name);
let conn = open_local_db()?; let conn = open_local_db()?;
let db_key = db::find_key(&conn, &db_key_name)?; let db_key = db::find_key(&conn, &db_key_name)?;
let db_key_value = match db_key { let db_key_value = match db_key {
None => return serve_common::error("key_name_not_exists"), None => return serve_common::client_error("name_not_exists"),
Some(k) => k, Some(k) => k,
}; };
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?; let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?;
drop(key); drop(key);
@@ -60,27 +89,23 @@ pub async fn write(req: Request<Body>) -> Result<Response<Body>> {
} }
async fn inner_write(req: Request<Body>) -> XResult<(StatusCode, Value)> { async fn inner_write(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let named_value: NamedValue = parse_request(req).await?;
let named_value: NamedValue = serde_json::from_reader(whole_body.reader())?;
let name = &named_value.name; let name = &named_value.name;
if name.is_empty() { if name.is_empty() {
return serve_common::error("key_name_is_empty"); return serve_common::client_error("name_is_empty");
} }
let db_key_name = db::make_db_key_name(name); let db_key_name = db::make_value_key_name(name);
let force_write = named_value.force_write.unwrap_or(false); let force_write = named_value.force_write.unwrap_or(false);
let conn = open_local_db()?; let conn = open_local_db()?;
let db_key = db::find_key(&conn, &db_key_name)?; let db_key = db::find_key(&conn, &db_key_name)?;
if db_key.is_some() && !force_write { if db_key.is_some() && !force_write {
return serve_common::error("key_name_exists"); return serve_common::client_error("name_exists");
} }
let value = named_value.value.to_bytes()?; let value = named_value.value.to_bytes()?;
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?; let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?;
drop(key); drop(key);

View File

@@ -8,18 +8,46 @@ use rust_util::{failure_and_exit, opt_result, success};
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
fn name(&self) -> &str { "yubikey-init-master-key" } fn name(&self) -> &str {
"yubikey-init-master-key"
}
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Local mini KMS init by Yubikey(HMAC)") SubCommand::with_name(self.name())
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge")) .about("Local mini KMS init by Yubikey(HMAC)")
.arg(Arg::with_name("key-hex").long("key-hex").short("x").takes_value(true).help("Key(hex), for encrypt")) .arg(
.arg(Arg::with_name("key-base64").long("key-base64").short("b").takes_value(true).help("Key(base64), for encrypt")) Arg::with_name("yubikey-challenge")
.arg(Arg::with_name("generate-key").long("generate-key").short("K").help("Generate key")) .long("yubikey-challenge")
.short("c")
.takes_value(true)
.help("Yubikey challenge"),
)
.arg(
Arg::with_name("key-hex")
.long("key-hex")
.short("x")
.takes_value(true)
.help("Key(hex), for encrypt"),
)
.arg(
Arg::with_name("key-base64")
.long("key-base64")
.short("b")
.takes_value(true)
.help("Key(base64), for encrypt"),
)
.arg(
Arg::with_name("generate-key")
.long("generate-key")
.short("K")
.help("Generate key"),
)
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let yubikey_challenge_opt = sub_arg_matches.value_of("yubikey-challenge").map(ToString::to_string); let yubikey_challenge_opt = sub_arg_matches
.value_of("yubikey-challenge")
.map(ToString::to_string);
let hex_value_opt = sub_arg_matches.value_of("key-hex"); let hex_value_opt = sub_arg_matches.value_of("key-hex");
let base64_value_opt = sub_arg_matches.value_of("key-base64"); let base64_value_opt = sub_arg_matches.value_of("key-base64");
@@ -31,10 +59,16 @@ impl Command for CommandImpl {
let clear_master_key = if let Some(hex_value) = hex_value_opt { let clear_master_key = if let Some(hex_value) = hex_value_opt {
opt_result!(hex::decode(hex_value), "Decode key-hex failed: {}") opt_result!(hex::decode(hex_value), "Decode key-hex failed: {}")
} else if let Some(base64_value) = base64_value_opt { } else if let Some(base64_value) = base64_value_opt {
opt_result!(STANDARD.decode(base64_value), "Decode key-base64 failed: {}") opt_result!(
STANDARD.decode(base64_value),
"Decode key-base64 failed: {}"
)
} else { } else {
let clear_master_key: [u8; 32] = random(); let clear_master_key: [u8; 32] = random();
success!("Clear master key generated: {}", hex::encode(clear_master_key)); success!(
"Clear master key generated: {}",
hex::encode(clear_master_key)
);
clear_master_key.to_vec() clear_master_key.to_vec()
}; };
@@ -42,17 +76,22 @@ impl Command for CommandImpl {
failure_and_exit!("Master key must be 32 bytes"); failure_and_exit!("Master key must be 32 bytes");
} }
let yubikey_challenge = yubikey_challenge_opt.unwrap_or_else( let yubikey_challenge = yubikey_challenge_opt.unwrap_or_else(|| {
|| match rpassword::prompt_password("Yubikey challenge: ") { match pinentry_util::read_pin(Some("Input yubikey challenge"), Some("Challenge: ")) {
Ok(yubikey_challenge) => yubikey_challenge, Ok(yubikey_challenge) => yubikey_challenge.get_pin().to_string(),
Err(e) => failure_and_exit!("Read yubikey challenge failed: {}", e), Err(e) => failure_and_exit!("Read yubikey challenge failed: {}", e),
} }
); });
let challenge_key = opt_result!( let challenge_key = opt_result!(
yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()), "Yubikey challenge failed: {}"); yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()),
"Yubikey challenge failed: {}"
);
let encrypted_master_key = opt_result!(jose::serialize_jwe_aes(&clear_master_key, &challenge_key), "Encrypt master key failed: {}"); let encrypted_master_key = opt_result!(
jose::serialize_jwe_aes(&clear_master_key, &challenge_key),
"Encrypt master key failed: {}"
);
success!("Encrypted master key: {}", encrypted_master_key); success!("Encrypted master key: {}", encrypted_master_key);
Ok(Some(0)) Ok(Some(0))