Compare commits
28 Commits
9d0e7548e6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
120526f6af
|
|||
|
7c760d26df
|
|||
|
a6a6674f30
|
|||
|
|
dc8616d644
|
||
|
|
9b2705966a
|
||
|
|
88154c9397
|
||
|
a8d3f6dadb
|
|||
|
4c7008ce26
|
|||
|
453e064139
|
|||
|
ffdecf0703
|
|||
|
26daa10c23
|
|||
|
c9ccd35053
|
|||
|
660a9e305d
|
|||
|
4796b53aae
|
|||
|
1cad0ee922
|
|||
|
a4ef732a2b
|
|||
|
aff9359172
|
|||
|
87cba2be57
|
|||
|
20ad9e6bd7
|
|||
|
ca0f18f2bc
|
|||
|
7d9f9f6870
|
|||
|
e2f5bc52a2
|
|||
|
b29374ce1d
|
|||
|
c939490f0e
|
|||
|
5e6694c53e
|
|||
|
909ac90eb9
|
|||
|
655f9f5ede
|
|||
|
82b38a2cf1
|
1162
Cargo.lock
generated
1162
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,38 +1,44 @@
|
||||
[package]
|
||||
name = "local-mini-kms"
|
||||
version = "1.0.0"
|
||||
version = "1.0.10"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["yubikey"]
|
||||
default = ["yubikey", "harden_process"]
|
||||
yubikey = ["yubico_manager"]
|
||||
harden_process = ["secmem-proc", "procfs"]
|
||||
|
||||
[dependencies]
|
||||
zeroize = "1.8"
|
||||
clap = "2.34"
|
||||
hex = "0.4"
|
||||
base64 = "0.21"
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
lazy_static = "1.5"
|
||||
serde_derive = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
secmem-proc = "0.3"
|
||||
secmem-proc = { version = "0.3", optional = true }
|
||||
seckey = "0.12"
|
||||
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"] }
|
||||
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 }
|
||||
rpassword = "7.3"
|
||||
rand = "0.8"
|
||||
rsa = "0.9"
|
||||
aes-kw = { version = "0.2", features = ["alloc"] }
|
||||
sha1 = "0.10"
|
||||
aes-gcm-stream = "0.2"
|
||||
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]
|
||||
procfs = "0.13"
|
||||
procfs = { version = "0.13", optional = true }
|
||||
|
||||
41
README.md
41
README.md
@@ -11,6 +11,7 @@ cargo build --release [--no-default-features]
|
||||
## Init
|
||||
|
||||
New random master key:
|
||||
|
||||
```shell
|
||||
head -c 32 /dev/random | base64
|
||||
```
|
||||
@@ -18,6 +19,7 @@ head -c 32 /dev/random | base64
|
||||
## Generate Yubikey encrypted master key
|
||||
|
||||
Generate encrypted master key with Yubikey:
|
||||
|
||||
```shell
|
||||
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 without init:
|
||||
|
||||
```shell
|
||||
local-mini-kms serve
|
||||
```
|
||||
|
||||
Init with Yubikey:
|
||||
|
||||
```shell
|
||||
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
|
||||
|
||||
```shell
|
||||
@@ -67,6 +77,7 @@ local-mini-kms cli --write --name test --value hello [--force-write] [--comment
|
||||
## cURL
|
||||
|
||||
Write value:
|
||||
|
||||
```shell
|
||||
curl -X POST http://127.0.0.1:5567/write \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -74,6 +85,7 @@ curl -X POST http://127.0.0.1:5567/write \
|
||||
```
|
||||
|
||||
Read value:
|
||||
|
||||
```shell
|
||||
curl -X POST http://127.0.0.1:5567/read \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -81,14 +93,39 @@ curl -X POST http://127.0.0.1:5567/read \
|
||||
```
|
||||
|
||||
Generate data key:
|
||||
|
||||
```shell
|
||||
curl -X POST http://127.0.0.1:5567/datakey \
|
||||
-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
|
||||
|
||||
```sql
|
||||
ALTER TABLE keys ADD COLUMN comment TEXT;
|
||||
ALTER TABLE keys
|
||||
ADD COLUMN comment TEXT;
|
||||
```
|
||||
|
||||
|
||||
7
justfile
7
justfile
@@ -1,6 +1,10 @@
|
||||
_:
|
||||
@just --list
|
||||
|
||||
# publish
|
||||
publish:
|
||||
cargo publish --registry crates-io
|
||||
|
||||
check:
|
||||
cargo check
|
||||
|
||||
@@ -10,3 +14,6 @@ lint:
|
||||
build:
|
||||
cargo build --release
|
||||
|
||||
build-linux-musl-with-zig:
|
||||
cargo zigbuild --release --target x86_64-unknown-linux-musl --no-default-features
|
||||
|
||||
|
||||
204
src/cli.rs
204
src/cli.rs
@@ -6,7 +6,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use hyper::body::Buf;
|
||||
use hyper::{Body, Client, Method, Request, Response, StatusCode};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
use rust_util::{debugging, 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 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("comment").long("comment").takes_value(true).help("Comment"))
|
||||
.arg(Arg::with_name("force-write").long("force-write").short("F").help("Force write value"))
|
||||
.arg(Arg::with_name("read-from-pinentry").long("read-from-pinentry").help("Read from pin-entry"))
|
||||
.arg(Arg::with_name("ssh-remote").long("ssh-remote").takes_value(true).help("SSH remote, root@example or localhost"))
|
||||
}
|
||||
|
||||
fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
|
||||
@@ -76,7 +78,24 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc
|
||||
} else if let Some(value_base64) = value_base64 {
|
||||
body_map.insert("clear_master_key_base64".to_string(), value_base64.into());
|
||||
} else {
|
||||
return simple_error!("Requires value hex or value base64");
|
||||
let pin = match pinentry_util::read_pin(
|
||||
Some("Input your clear master key, starts with hex: or base64:"),
|
||||
Some("Clear master key: ")) {
|
||||
Ok(pin) => pin,
|
||||
Err(e) => return simple_error!("Read clear master key failed: {}", e),
|
||||
};
|
||||
let pin_str = pin.get_pin();
|
||||
let clear_master_key = if pin_str.starts_with("hex:") {
|
||||
let hex: String = pin_str.chars().skip(4).collect();
|
||||
hex::decode(&hex)?
|
||||
} else if pin_str.starts_with("base64:") {
|
||||
let base64: String = pin_str.chars().skip(7).collect();
|
||||
STANDARD.decode(&base64)?
|
||||
} else {
|
||||
return simple_error!("Clear master key must starts with hex: or base64:");
|
||||
};
|
||||
|
||||
body_map.insert("clear_master_key_hex".to_string(), hex::encode(&clear_master_key).into());
|
||||
}
|
||||
if let Some(yubikey_challenge) = yubikey_challenge {
|
||||
body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into());
|
||||
@@ -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 {
|
||||
let ssh_remote = sub_arg_matches.value_of("ssh-remote").map(|s| s.to_string());
|
||||
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
|
||||
let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
|
||||
|
||||
let client = Client::new();
|
||||
let uri = format!("http://{}/status", connect);
|
||||
debugging!("Request uri: {}", &uri);
|
||||
let 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?;
|
||||
|
||||
let data = send_kms_request_with_ssh_enabled(&ssh_remote, true, &uri, &None).await?;
|
||||
|
||||
debugging!("Get status: {}", &data);
|
||||
let status = &data["status"];
|
||||
if let Some(status) = status.as_str() {
|
||||
@@ -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"];
|
||||
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);
|
||||
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);
|
||||
}
|
||||
let _ = send_kms_request_with_ssh_enabled(&ssh_remote, false, &uri, &Some(body)).await?;
|
||||
|
||||
success!("Init finished");
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn card_hmac_decrypt(ciphertext: &str) -> XResult<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 {
|
||||
let body = if let Some(name) = sub_arg_matches.value_of("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");
|
||||
};
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -193,30 +283,59 @@ async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
|
||||
let body = json!({ "encrypted_value": value });
|
||||
|
||||
let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?;
|
||||
success!("Value: {}", serde_json::to_string_pretty(&data)?);
|
||||
println!("{}", serde_json::to_string_pretty(&data)?);
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn do_offline_init(_arg_matches: &ArgMatches<'_>, _sub_arg_matches: &ArgMatches<'_>) -> CommandError {
|
||||
let line = read_line("Input master key: ")?;
|
||||
let master_key = if line.starts_with("hex:") {
|
||||
let hex: String = line.chars().skip(4).collect();
|
||||
hex::decode(&hex)?
|
||||
} else if line.starts_with("base64:") {
|
||||
let base64: String = line.chars().skip(7).collect();
|
||||
STANDARD.decode(&base64)?
|
||||
} else {
|
||||
line.as_bytes().to_vec()
|
||||
};
|
||||
let jwk = read_line("Input JWK: ")?;
|
||||
let rsa_public_key = jwk_to_rsa_pubic_key(&jwk)?;
|
||||
let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &rsa_public_key)?;
|
||||
fn do_offline_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
|
||||
let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
|
||||
|
||||
let line = read_line("Input master key: ", read_from_pinentry)?;
|
||||
let jwk = read_line("Input JWK: ", read_from_pinentry)?;
|
||||
|
||||
let encrypted_master_key = master_key_encrypt(&line, &jwk)?;
|
||||
|
||||
success!("Encrypted master key: {}", encrypted_master_key);
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn 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().flush().ok();
|
||||
let mut line = String::new();
|
||||
@@ -226,6 +345,11 @@ fn read_line(prompt: &str) -> XResult<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> {
|
||||
let req_body = response.into_body();
|
||||
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_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);
|
||||
}
|
||||
// if req_response.status() != StatusCode::OK {
|
||||
// let status = req_response.status().as_u16();
|
||||
// let data = response_to_value(req_response).await?;
|
||||
// return simple_error!("Server status is not success: {}, response: {}", status, data);
|
||||
// }
|
||||
response_to_value(req_response).await
|
||||
}
|
||||
|
||||
150
src/db.rs
150
src/db.rs
@@ -1,5 +1,5 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use rust_util::{debugging, information, opt_result, simple_error, success, XResult};
|
||||
use rusqlite::{params, Connection};
|
||||
use rust_util::{opt_result, simple_error, XResult};
|
||||
|
||||
pub const DEFAULT_MASTER_KEY_VERIFICATION_KEY: &str = "__master_verification_key";
|
||||
|
||||
@@ -9,47 +9,111 @@ pub struct Key {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn make_data_key_name(name: &str) -> String {
|
||||
format!("data_key:{}", name)
|
||||
}
|
||||
|
||||
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());
|
||||
log::debug!("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);
|
||||
if let Ok(false) = check_table_keys(conn) {
|
||||
repair_table_keys(conn)?;
|
||||
}
|
||||
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT,
|
||||
comment TEXT
|
||||
)
|
||||
"##, ())?;
|
||||
success!("Table keys created");
|
||||
Ok(true)
|
||||
)"##,
|
||||
(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> {
|
||||
let default_comment = "".to_string();
|
||||
log::debug!("insert key name={}", &key.name);
|
||||
let _ = conn.execute(
|
||||
"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(())
|
||||
}
|
||||
|
||||
pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> {
|
||||
log::debug!("update key name={}", &key.name);
|
||||
if let Some(comment) = &key.comment {
|
||||
let _ = conn.execute(
|
||||
"UPDATE keys SET value = ?1, comment = ?2 WHERE name = ?3",
|
||||
@@ -64,6 +128,48 @@ pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> {
|
||||
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>> {
|
||||
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| {
|
||||
@@ -74,8 +180,14 @@ pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> {
|
||||
})
|
||||
})?;
|
||||
match key_iter.next() {
|
||||
None => Ok(None),
|
||||
Some(Ok(r)) => Ok(Some(r)),
|
||||
None => {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
169
src/jose.rs
169
src/jose.rs
@@ -7,7 +7,7 @@ use rand::{random, thread_rng};
|
||||
use rsa::pkcs1::LineEnding;
|
||||
use rsa::pkcs8::EncodePublicKey;
|
||||
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 sha1::Sha1;
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -18,12 +18,17 @@ const JWE_ALG_A256KW: &str = "A256KW";
|
||||
const JWE_ALG_RSA_OAEP: &str = "RSA-OAEP";
|
||||
const JWE_DOT: &str = ".";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct JweHeader {
|
||||
pub enc: String,
|
||||
pub alg: String,
|
||||
pub vendor: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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> {
|
||||
@@ -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> {
|
||||
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);
|
||||
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(),
|
||||
alg: JWE_ALG_RSA_OAEP.to_string(),
|
||||
vendor: "local-mini-kms".to_string(),
|
||||
version: None,
|
||||
..Default::default()
|
||||
};
|
||||
serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> {
|
||||
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 {
|
||||
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> {
|
||||
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 {
|
||||
enc: JWE_ENC_A256GCM.to_string(),
|
||||
alg: JWE_ALG_A256KW.to_string(),
|
||||
vendor: "local-mini-kms".to_string(),
|
||||
version: Some(get_master_key_checksum(&key)),
|
||||
data_type,
|
||||
exportable,
|
||||
};
|
||||
serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> {
|
||||
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);
|
||||
}
|
||||
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,
|
||||
header_b64,
|
||||
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(&tag)
|
||||
))
|
||||
@@ -175,9 +186,7 @@ fn to_bytes32(bytes: &[u8]) -> XResult<[u8; 32]> {
|
||||
return simple_error!("Not valid 32 bytes");
|
||||
}
|
||||
let mut ret = [0; 32];
|
||||
for i in 0..32 {
|
||||
ret[i] = bytes[i];
|
||||
}
|
||||
ret.copy_from_slice(&bytes[..32]);
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
@@ -198,3 +207,137 @@ fn get_jwe(jwe: &str) -> 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"));
|
||||
}
|
||||
|
||||
12
src/main.rs
12
src/main.rs
@@ -2,6 +2,11 @@ use clap::{App, AppSettings, ArgMatches};
|
||||
use rust_util::util_clap::{Command, CommandError};
|
||||
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 proc;
|
||||
mod jose;
|
||||
@@ -17,6 +22,7 @@ mod yubikey_hmac;
|
||||
#[cfg(feature = "yubikey")]
|
||||
mod yubikey_init_master_key;
|
||||
mod serve_datakey;
|
||||
mod serve_log;
|
||||
|
||||
pub struct DefaultCommandImpl;
|
||||
|
||||
@@ -36,6 +42,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "harden_process")]
|
||||
fn harden_process() {
|
||||
let ignore_harden_process_error = std::env::var("IGNORE_HARDEN_PROCESS_ERROR")
|
||||
.map(|v| &v == "true").unwrap_or_else(|_| false);
|
||||
@@ -61,6 +68,8 @@ fn inner_main() -> CommandError {
|
||||
{
|
||||
#[cfg(feature = "yubikey")]
|
||||
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 mut app = App::new(env!("CARGO_PKG_NAME"))
|
||||
@@ -75,9 +84,10 @@ fn inner_main() -> CommandError {
|
||||
let matches = app.get_matches();
|
||||
for command in &commands {
|
||||
if let Some(sub_cmd_matches) = matches.subcommand_matches(command.name()) {
|
||||
#[cfg(feature = "harden_process")]
|
||||
if command.name() == "serve" { harden_process(); }
|
||||
return command.run(&matches, sub_cmd_matches);
|
||||
}
|
||||
}
|
||||
DefaultCommandImpl::run(&matches)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ pub struct Process {
|
||||
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> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", feature = "harden_process"))]
|
||||
pub fn get_process(port: u16) -> Option<Process> {
|
||||
let all_procs = match procfs::process::all_processes() {
|
||||
Err(e) => {
|
||||
|
||||
196
src/serve.rs
196
src/serve.rs
@@ -1,5 +1,3 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use hyper::client::HttpConnector;
|
||||
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::{failure_and_exit, information, success, warning, XResult};
|
||||
use serde_json::{json, Value};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::serve_common::{self, GenericError, MemoryKey, Result};
|
||||
use crate::serve_encrypt_decrypt;
|
||||
use crate::serve_init;
|
||||
use crate::serve_init::InitRequest;
|
||||
use crate::serve_read_write;
|
||||
@@ -20,22 +19,77 @@ use crate::serve_status;
|
||||
use crate::yubikey_hmac;
|
||||
use crate::{db, jose, proc};
|
||||
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;
|
||||
|
||||
impl Command for CommandImpl {
|
||||
fn name(&self) -> &str { "serve" }
|
||||
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").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"))
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Local mini KMS serve")
|
||||
.arg(
|
||||
Arg::with_name("listen")
|
||||
.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 {
|
||||
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) {
|
||||
Ok(true) => success!("Init server success"),
|
||||
Ok(false) => failure_and_exit!("SHOULD NOT HAPPEN, server already init"),
|
||||
@@ -46,9 +100,13 @@ impl Command for CommandImpl {
|
||||
#[cfg(feature = "yubikey")]
|
||||
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 {
|
||||
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 new_service = make_service_fn(move |conn: &AddrStream| {
|
||||
let remote_addr = conn.remote_addr();
|
||||
@@ -61,6 +119,7 @@ impl Command for CommandImpl {
|
||||
});
|
||||
let server = Server::bind(&addr).serve(new_service);
|
||||
information!("Listening on http://{}", addr);
|
||||
log::info!("Listening on http://{}", addr);
|
||||
match server.await {
|
||||
Err(e) => failure_and_exit!("Server error: {}", e),
|
||||
Ok(_) => success!("Server ended"),
|
||||
@@ -77,11 +136,19 @@ async fn response_requests(
|
||||
req: Request<Body>,
|
||||
_client: Client<HttpConnector>,
|
||||
) -> Result<Response<Body>> {
|
||||
let request_idx = GLOBAL_REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
let process = proc::get_process(remote_addr.port());
|
||||
match process {
|
||||
None => information!( "Receive request: {}, from: {}", req.uri(), remote_addr ),
|
||||
Some(process) => information!(
|
||||
"Receive request: {}, from: {}, process: {} {} {:?}",
|
||||
None => log::info!(
|
||||
"[{:06}] Receive request: {} {}, from: {}",
|
||||
request_idx,
|
||||
req.method(),
|
||||
req.uri(),
|
||||
remote_addr
|
||||
),
|
||||
Some(process) => log::info!(
|
||||
"[{:06}] Receive request: {}, from: {}, process: {} {} {:?}",
|
||||
request_idx,
|
||||
req.uri(),
|
||||
remote_addr,
|
||||
process.pid,
|
||||
@@ -94,17 +161,22 @@ async fn response_requests(
|
||||
(&Method::POST, "/update") => update().await,
|
||||
(&Method::POST, "/decrypt") => serve_encrypt_decrypt::decrypt(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, "/write") => serve_read_write::write(req).await,
|
||||
(&Method::POST, "/datakey") => serve_datakey::generate(req).await,
|
||||
(&Method::GET, "/status") => serve_status::status().await,
|
||||
(&Method::GET, "/version") => get_version().await,
|
||||
(&Method::GET, "/") => get_root().await,
|
||||
_ => Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(format!("{}\n", serde_json::to_string_pretty(&json!({
|
||||
"error": "not_found",
|
||||
}))?).into())?),
|
||||
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND).body(
|
||||
format!(
|
||||
"{}\n",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"error": "not_found",
|
||||
}))?
|
||||
)
|
||||
.into(),
|
||||
)?),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +186,9 @@ fn init_instance(db: &str) -> XResult<bool> {
|
||||
let conn = db::open_db(db)?;
|
||||
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 {
|
||||
Some(_) => Ok(false),
|
||||
None => {
|
||||
@@ -130,7 +204,9 @@ fn init_instance(db: &str) -> 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 {
|
||||
Some(k) => {
|
||||
k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?;
|
||||
@@ -146,21 +222,30 @@ async fn update() -> Result<Response<Body>> {
|
||||
|
||||
async fn inner_update() -> XResult<(StatusCode, Value)> {
|
||||
let update = update_instance_rsa_key_pair()?;
|
||||
Ok((StatusCode::OK, json!({
|
||||
"update": update,
|
||||
})))
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
json!({
|
||||
"update": update,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_version() -> Result<Response<Body>> {
|
||||
Ok(Response::builder().body(format!(
|
||||
"{} - {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")
|
||||
).into())?)
|
||||
Ok(Response::builder().body(
|
||||
format!(
|
||||
"{} - {}\n",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)
|
||||
.into(),
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn get_root() -> Result<Response<Body>> {
|
||||
if std::env::var("LOCAL_MINI_KMS_HELP").is_ok() {
|
||||
Ok(Response::builder().body(format!(
|
||||
r##"{} - {}
|
||||
Ok(Response::builder().body(
|
||||
format!(
|
||||
r##"{} - {}
|
||||
Supports commands:
|
||||
- GET /version
|
||||
- GET /status
|
||||
@@ -170,8 +255,12 @@ Supports commands:
|
||||
- POST /decrypt
|
||||
- POST /read
|
||||
- POST /write
|
||||
"##, env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")
|
||||
).into())?)
|
||||
"##,
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)
|
||||
.into(),
|
||||
)?)
|
||||
} else {
|
||||
Ok(Response::builder().body("Root Not Found\n".into())?)
|
||||
}
|
||||
@@ -179,29 +268,37 @@ Supports commands:
|
||||
|
||||
#[cfg(feature = "yubikey")]
|
||||
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");
|
||||
|
||||
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) {
|
||||
(Some(yubikey_challenge), Some(init_encrypted_master_key)) => {
|
||||
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
|
||||
Err(e) => {
|
||||
warning!("Yubikey challenge failed: {}", e);
|
||||
return;
|
||||
let (challenge_key, init_encrypted_master_key) =
|
||||
match (yubikey_challenge, init_encrypted_master_key) {
|
||||
(Some(yubikey_challenge), Some(init_encrypted_master_key)) => {
|
||||
match yubikey_hmac::yubikey_challenge_as_32_bytes(yubikey_challenge.as_bytes()) {
|
||||
Err(e) => {
|
||||
warning!("Yubikey challenge failed: {}", e);
|
||||
return;
|
||||
}
|
||||
Ok(challenge_key) => (challenge_key, init_encrypted_master_key),
|
||||
}
|
||||
Ok(challenge_key) => (challenge_key, init_encrypted_master_key),
|
||||
}
|
||||
}
|
||||
(Some(_), None) | (None, Some(_)) => {
|
||||
warning!("Arguments yubikey-challenge and init-encrypted-master-key should both assigned.");
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
(Some(_), None) | (None, Some(_)) => {
|
||||
warning!(
|
||||
"Arguments yubikey-challenge and init-encrypted-master-key should both assigned."
|
||||
);
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match jose::deserialize_jwe_aes(init_encrypted_master_key, &challenge_key) {
|
||||
Err(e) => warning!("Yubikey seal master key failed: {}", e),
|
||||
@@ -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_base64: None,
|
||||
encrypted_master_key: None,
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
});
|
||||
match init_master_key_result {
|
||||
Err(e) => warning!("Init master key failed: {}", e),
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
use std::io::{Cursor, Read};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use hyper::StatusCode;
|
||||
use hyper::body::Buf;
|
||||
use hyper::{Body, Request, StatusCode};
|
||||
use rsa::RsaPrivateKey;
|
||||
use rusqlite::Connection;
|
||||
use rust_util::{opt_result, simple_error, XResult};
|
||||
use seckey::SecBytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
use crate::db;
|
||||
|
||||
pub type GenericError = Box<dyn std::error::Error + Send + Sync>;
|
||||
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_rules! do_response {
|
||||
($ex: expr) => (
|
||||
@@ -34,11 +55,12 @@ pub fn ok(body: Value) -> XResult<(StatusCode, Value)> {
|
||||
Ok((StatusCode::OK, body))
|
||||
}
|
||||
|
||||
pub fn error(error: &str) -> XResult<(StatusCode, Value)> {
|
||||
Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
json!({ "error": error })
|
||||
))
|
||||
pub fn client_error(error: &str) -> XResult<(StatusCode, Value)> {
|
||||
Ok((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)> {
|
||||
@@ -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> {
|
||||
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 {
|
||||
None => None,
|
||||
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> {
|
||||
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 {
|
||||
None => simple_error!("Db is not initted!"),
|
||||
Some(k) => {
|
||||
Ok(opt_result!(db::open_db(& k.database_file), "Open db failed: {}"))
|
||||
}
|
||||
None => simple_error!("Db is not initiated!"),
|
||||
Some(k) => Ok(opt_result!(
|
||||
db::open_db(&k.database_file),
|
||||
"Open db failed: {}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
use crate::serve_common::{get_master_key, Result};
|
||||
use crate::{do_response, jose, serve_common};
|
||||
use crate::db::Key;
|
||||
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;
|
||||
use hyper::body::Buf;
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use rand::random;
|
||||
use rust_util::{debugging, iff, XResult};
|
||||
use rust_util::{iff, XResult};
|
||||
use seckey::SecBytes;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
|
||||
// type:aes, spec:128,192,256
|
||||
// type:rsa, spec:2048,3072,4096
|
||||
// type:ec, spec:p256,p384,p521,ed25519,cv25519
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DataKeyRequest {
|
||||
key_type: String,
|
||||
key_spec: String,
|
||||
r#type: String,
|
||||
spec: String,
|
||||
name: Option<String>,
|
||||
comment: Option<String>,
|
||||
exportable: 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)> {
|
||||
let whole_body = hyper::body::aggregate(req).await?;
|
||||
let request: DataKeyRequest = serde_json::from_reader(whole_body.reader())?;
|
||||
let request: DataKeyRequest = parse_request(req).await?;
|
||||
|
||||
debugging!("Generate data key: {} {}", &request.key_type, &request.key_spec);
|
||||
let key = match get_master_key() {
|
||||
None => return serve_common::error("status_not_ready"),
|
||||
Some(key) => key,
|
||||
};
|
||||
let ret_key_plaintext = request.return_plaintext.unwrap_or(false);
|
||||
log::debug!("Generate data key: {} {}", &request.r#type, &request.spec);
|
||||
let key = require_master_key!();
|
||||
let exportable = request.exportable.unwrap_or(true);
|
||||
let ret_key_plaintext = iff!(!exportable, false, request.return_plaintext.unwrap_or(false));
|
||||
|
||||
let response_result = match (request.key_type.as_str(), request.key_spec.as_str()) {
|
||||
("aes", "128") => generate_aes("datakey.aes_128:", key, 16, ret_key_plaintext),
|
||||
("aes", "192") => generate_aes("datakey.aes_192:", key, 24, ret_key_plaintext),
|
||||
("aes", "256") => generate_aes("datakey.aes_256:", key, 32, ret_key_plaintext),
|
||||
let response_result = match (request.r#type.as_str(), request.spec.as_str()) {
|
||||
// ("aes", "128") => generate_aes("datakey:aes-128", exportable, key, 16, ret_key_plaintext),
|
||||
// ("aes", "192") => generate_aes("datakey:aes-192", exportable, key, 24, ret_key_plaintext),
|
||||
("aes", "256") => generate_aes("datakey:aes-256", exportable, key, 32, ret_key_plaintext),
|
||||
// TODO rsa 2048, rsa 3072, rsa 4096
|
||||
// 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 {
|
||||
Err(e) => serve_common::error(&format!("internal error: {}", e)),
|
||||
Err(e) => serve_common::server_error(&format!("internal error: {}", e)),
|
||||
Ok((key_plaintext, key_ciphertext)) => {
|
||||
let mut map = Map::new();
|
||||
map.insert("key_type".to_string(), Value::String(request.key_type));
|
||||
map.insert("key_spec".to_string(), Value::String(request.key_spec));
|
||||
map.insert("key_type".to_string(), Value::String(request.r#type));
|
||||
map.insert("key_spec".to_string(), Value::String(request.spec));
|
||||
if let Some(key_plaintext) = 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 value = &bytes[0..len as usize];
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use hyper::body::Buf;
|
||||
use rust_util::{debugging, XResult};
|
||||
use rust_util::XResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::do_response;
|
||||
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)]
|
||||
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)> {
|
||||
let whole_body = hyper::body::aggregate(req).await?;
|
||||
let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?;
|
||||
let data: DecryptRequest = parse_request(req).await?;
|
||||
|
||||
debugging!("To be decrypted value: {}", &data.encrypted_value);
|
||||
let key = match get_master_key() {
|
||||
None => return serve_common::error("status_not_ready"),
|
||||
Some(key) => key,
|
||||
};
|
||||
log::trace!("To be decrypted value: {}", &data.encrypted_value);
|
||||
let key = require_master_key!();
|
||||
let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key.read());
|
||||
drop(key);
|
||||
let (data, header) = decrypted_value?;
|
||||
|
||||
decrypted_value.map(|v| {
|
||||
let map = byte_to_multi_view_map(&v.0, true);
|
||||
(StatusCode::OK, Value::Object(map))
|
||||
})
|
||||
if let Some(false) = header.exportable {
|
||||
return serve_common::client_error("data_not_exportable");
|
||||
}
|
||||
|
||||
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>> {
|
||||
@@ -40,13 +50,9 @@ pub async fn encrypt(req: Request<Body>) -> Result<Response<Body>> {
|
||||
}
|
||||
|
||||
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 data: MultipleViewValue = parse_request(req).await?;
|
||||
let value = data.to_bytes()?;
|
||||
let key = match get_master_key() {
|
||||
None => return serve_common::error("status_not_ready"),
|
||||
Some(key) => key,
|
||||
};
|
||||
let key = require_master_key!();
|
||||
let encrypt_result = jose::serialize_jwe_aes(&value, &key.read());
|
||||
drop(key);
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use hyper::body::Buf;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -10,7 +9,7 @@ use zeroize::Zeroize;
|
||||
|
||||
use crate::db::Key;
|
||||
use crate::do_response;
|
||||
use crate::serve_common::{self, Result};
|
||||
use crate::serve_common::{self, parse_request, Result};
|
||||
#[cfg(feature = "yubikey")]
|
||||
use crate::yubikey_hmac;
|
||||
use crate::{db, jose};
|
||||
@@ -28,18 +27,16 @@ pub struct InitRequest {
|
||||
}
|
||||
|
||||
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 init_request: InitRequest = parse_request(req).await?;
|
||||
inner_init_request(init_request).await
|
||||
}
|
||||
|
||||
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");
|
||||
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() {
|
||||
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 {
|
||||
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);
|
||||
log::debug!("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)?;
|
||||
clear_master_key
|
||||
} 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 {
|
||||
return serve_common::error("master_key_missing");
|
||||
return serve_common::client_error("master_key_missing");
|
||||
};
|
||||
|
||||
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 {
|
||||
@@ -71,24 +68,24 @@ pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCod
|
||||
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)?,
|
||||
comment: None,
|
||||
comment: Some("local-mini-kms re-init verification".to_string()),
|
||||
};
|
||||
db::insert_key(&conn, &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: {}");
|
||||
}
|
||||
}
|
||||
information!("Set master key success");
|
||||
log::info!("Set master key success");
|
||||
|
||||
#[cfg(feature = "yubikey")]
|
||||
if let Some(yubikey_challenge) = &init_request.yubikey_challenge {
|
||||
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) {
|
||||
Err(e) => warning!("Yubikey seal master key failed: {}", e),
|
||||
Ok(jwe) => success!("Yubikey sealed master key: {}", jwe)
|
||||
Err(e) => log::warn!("Yubikey seal master key failed: {}", e),
|
||||
Ok(jwe) => log::info!("Yubikey sealed master key: {}", jwe)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
66
src/serve_log.rs
Normal file
66
src/serve_log.rs
Normal 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");
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
use crate::db::Key;
|
||||
use crate::do_response;
|
||||
use crate::serve_common::{self, byte_to_multi_view_map, get_master_key, open_local_db, MultipleViewValue, Result};
|
||||
use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, parse_request, MultipleViewValue, Result};
|
||||
use crate::{db, jose};
|
||||
use hyper::body::Buf;
|
||||
use crate::{do_response, require_master_key};
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use rust_util::XResult;
|
||||
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)]
|
||||
struct Named {
|
||||
@@ -21,31 +27,54 @@ struct NamedValue {
|
||||
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>> {
|
||||
do_response!(inner_read(req).await)
|
||||
}
|
||||
|
||||
async fn inner_read(req: Request<Body>) -> XResult<(StatusCode, Value)> {
|
||||
let whole_body = hyper::body::aggregate(req).await?;
|
||||
let named: Named = serde_json::from_reader(whole_body.reader())?;
|
||||
let named: Named = parse_request(req).await?;
|
||||
let name = &named.name;
|
||||
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 db_key = db::find_key(&conn, &db_key_name)?;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
let key = match get_master_key() {
|
||||
None => return serve_common::error("status_not_ready"),
|
||||
Some(key) => key,
|
||||
};
|
||||
let key = require_master_key!();
|
||||
let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?;
|
||||
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)> {
|
||||
let whole_body = hyper::body::aggregate(req).await?;
|
||||
let named_value: NamedValue = serde_json::from_reader(whole_body.reader())?;
|
||||
let named_value: NamedValue = parse_request(req).await?;
|
||||
let name = &named_value.name;
|
||||
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 conn = open_local_db()?;
|
||||
let db_key = db::find_key(&conn, &db_key_name)?;
|
||||
|
||||
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 key = match get_master_key() {
|
||||
None => return serve_common::error("status_not_ready"),
|
||||
Some(key) => key,
|
||||
};
|
||||
let key = require_master_key!();
|
||||
let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?;
|
||||
drop(key);
|
||||
|
||||
|
||||
@@ -8,18 +8,46 @@ use rust_util::{failure_and_exit, opt_result, success};
|
||||
pub struct 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> {
|
||||
SubCommand::with_name(self.name()).about("Local mini KMS init by Yubikey(HMAC)")
|
||||
.arg(Arg::with_name("yubikey-challenge").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"))
|
||||
SubCommand::with_name(self.name())
|
||||
.about("Local mini KMS init by Yubikey(HMAC)")
|
||||
.arg(
|
||||
Arg::with_name("yubikey-challenge")
|
||||
.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 {
|
||||
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 base64_value_opt = sub_arg_matches.value_of("key-base64");
|
||||
@@ -29,12 +57,18 @@ impl Command for CommandImpl {
|
||||
failure_and_exit!("--key-hex, --key-base64 or --generate-key must assign one");
|
||||
}
|
||||
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 {
|
||||
opt_result!(STANDARD.decode(base64_value), "Decode key-base64 failed: {}")
|
||||
opt_result!(
|
||||
STANDARD.decode(base64_value),
|
||||
"Decode key-base64 failed: {}"
|
||||
)
|
||||
} else {
|
||||
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()
|
||||
};
|
||||
|
||||
@@ -42,19 +76,24 @@ impl Command for CommandImpl {
|
||||
failure_and_exit!("Master key must be 32 bytes");
|
||||
}
|
||||
|
||||
let yubikey_challenge = yubikey_challenge_opt.unwrap_or_else(
|
||||
|| match rpassword::prompt_password("Yubikey challenge: ") {
|
||||
Ok(yubikey_challenge) => yubikey_challenge,
|
||||
let yubikey_challenge = yubikey_challenge_opt.unwrap_or_else(|| {
|
||||
match pinentry_util::read_pin(Some("Input yubikey challenge"), Some("Challenge: ")) {
|
||||
Ok(yubikey_challenge) => yubikey_challenge.get_pin().to_string(),
|
||||
Err(e) => failure_and_exit!("Read yubikey challenge failed: {}", e),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
Ok(Some(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user