Compare commits

...

72 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
9d0e7548e6 Merge pull request 'remove-openssl-dep' (#6) from remove-openssl-dep into master
Reviewed-on: #6
2024-11-14 22:57:44 +08:00
c64f6a9836 feat: v1.0.0, opt cli 2024-11-14 22:57:13 +08:00
f2ed407181 feat: v0.3.8, remove openssl dependency 2024-11-14 01:31:15 +08:00
9fe642ba7a feat: update dependencies 2024-11-14 00:00:12 +08:00
ec7aaa9fca feat: v0.3.7, yubikey is optional 2024-11-10 18:38:56 +08:00
8918d1d59b feat: add justfile 2024-11-10 00:15:02 +08:00
2e706484ea feat: update readme 2024-11-10 00:12:58 +08:00
78cfc3ff2b feat: update help 2024-11-10 00:08:50 +08:00
c14ebc047a feat: update dependencies 2024-11-09 23:56:04 +08:00
b1121ffeeb feat: v0.3.6, add generate aes(128,192,256 bit) datakey 2024-09-04 00:14:59 +08:00
b188a2bc1e feat: v0.3.5, add subcommand yubikey-init-master-key 2024-09-03 23:19:17 +08:00
d6c49a15ea feat: make clippy happy & update denpendencies 2024-09-03 22:37:32 +08:00
15201b92a5 v0.3.3 2023-08-14 00:09:32 +08:00
9afca20456 v0.3.3 2023-08-14 00:00:11 +08:00
eff78bcf21 v0.3.3 2023-08-13 23:45:57 +08:00
ce67343cdf v0.3.2 2023-08-13 23:04:07 +08:00
f3d83c7d62 v0.3.2 2023-08-13 23:01:08 +08:00
7f6b8ab819 feat: v3.2 2023-08-13 22:58:59 +08:00
56f6ccd777 feat: v0.3.1 2023-08-13 21:39:31 +08:00
b5e13dc13a feat: supports yubikey init 2023-08-13 21:26:16 +08:00
e16c28f2ab feat: update versions 2023-08-13 16:52:29 +08:00
420cbd5087 feat: update 2023-08-13 16:42:11 +08:00
0cdfd4d7d0 feat: update 2023-08-13 16:33:05 +08:00
05446372cf feat: cli read/write 2023-08-13 16:06:48 +08:00
167e37f9f8 feat: add read/write 2023-08-13 15:26:49 +08:00
b431a94ef9 feat: update dependencies 2023-03-25 23:37:27 +08:00
010b8c538f feat: update readme 2022-10-01 23:20:12 +08:00
611b50f380 feat: update versions 2022-10-01 23:12:17 +08:00
0351735531 feat: update versions 2022-10-01 18:01:04 +08:00
487739515b feat: update response 2022-07-30 12:17:06 +08:00
8421bd61f3 feat: update response 2022-07-30 12:16:00 +08:00
f617bfbab2 feat: update procfs 2022-07-29 23:54:43 +08:00
fe2a02c043 feat: update procfs 2022-07-29 00:43:55 +08:00
d6189b5af2 feat: update procfs 2022-07-29 00:43:21 +08:00
d66dac29ac feat: update procfs 2022-07-29 00:43:03 +08:00
2df8b55620 feat: update procfs 2022-07-29 00:42:33 +08:00
51b0bb96f3 feat: update procfs 2022-07-29 00:41:57 +08:00
925f186817 feat: update procfs 2022-07-29 00:41:29 +08:00
ffb630bb64 feat: add procfs 2022-07-29 00:35:05 +08:00
f598db686b feat: print remote addr 2022-07-29 00:08:36 +08:00
ad38744930 feat: update dependencies 2022-07-28 00:09:42 +08:00
4bab656dfb feat: v0.2.1 add seckey support 2022-07-27 23:52:25 +08:00
77591990ad feat: v0.2.0, add harden support 2022-07-27 23:31:25 +08:00
7af1521354 feat: add --direct-iit 2022-07-25 01:52:22 +08:00
20 changed files with 4077 additions and 780 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
local-mini-kms
LOCAL_README.md
*_gitignore.*
local-mini-kms.db local-mini-kms.db
.idea/ .idea/
# ---> Rust # ---> Rust

2404
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,44 @@
[package] [package]
name = "local-mini-kms" name = "local-mini-kms"
version = "0.1.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]
default = ["yubikey", "harden_process"]
yubikey = ["yubico_manager"]
harden_process = ["secmem-proc", "procfs"]
[dependencies] [dependencies]
zeroize = "1.5.7" zeroize = "1.8"
clap = "2.33" clap = "2.34"
hex = "0.4" hex = "0.4"
base64 = "0.13.0" base64 = "0.22"
sha2 = "0.10.2" sha2 = "0.10"
lazy_static = "1.4.0" 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"
josekit = "0.8.1" secmem-proc = { version = "0.3", optional = true }
seckey = "0.12"
rust_util = { version = "0.6", features = ["use_clap"] } rust_util = { version = "0.6", features = ["use_clap"] }
tokio = { version = "1.19", features = ["full"] } tokio = { version = "1.45", features = ["full"] }
hyper = { version = "0.14.20", features = ["client", "server", "tcp", "http1", "http2"] } hyper = { version = "0.14", features = ["client", "server", "tcp", "http1", "http2"] }
rusqlite = "0.28.0" # use bundled fix musl build core dump on CentOS 6.9
rusqlite = { version = "0.32", features = ["bundled"] }
yubico_manager = { version = "0.9", optional = true }
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 = { version = "0.13", optional = true }

128
README.md
View File

@@ -1,3 +1,131 @@
# local-mini-kms # local-mini-kms
Mini-KMS runs local written by Rust Mini-KMS runs local written by Rust
## Build
```shell
cargo build --release [--no-default-features]
```
## Init
New random master key:
```shell
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*]
```
## 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
local-mini-kms cli --init
```
```shell
local-mini-kms cli --offline-init
```
```shell
local-mini-kms cli --direct-init --value-base64 wNdr9sZN4**** [--yubikey-challenge *challenge*]
```
```shell
local-mini-kms cli --encrypt --value hello
```
```shell
local-mini-kms cli --decrypt --value LKMS:***
```
```shell
local-mini-kms cli --read --name test
```
```shell
local-mini-kms cli --write --name test --value hello [--force-write] [--comment *comment*]
```
## cURL
Write value:
```shell
curl -X POST http://127.0.0.1:5567/write \
-H "Content-Type: application/json" \
-d '{"name":"test","value":{"value":"hello"}}'
```
Read value:
```shell
curl -X POST http://127.0.0.1:5567/read \
-H "Content-Type: application/json" \
-d '{"name":"test"}'
```
Generate data key:
```shell
curl -X POST http://127.0.0.1:5567/datakey \
-H "Content-Type: application/json" \
-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;
```

19
justfile Normal file
View File

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

View File

@@ -1,14 +1,16 @@
use std::io::Write; use std::io::Write;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use hyper::{Body, Client, Method, Request, Response, StatusCode};
use hyper::body::Buf; use hyper::body::Buf;
use josekit::jwk::Jwk; use hyper::{Body, Client, Method, Request, Response, StatusCode};
use rust_util::{debugging, opt_value_result, simple_error, success, XResult};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use serde_json::{json, Value}; use rust_util::{debugging, iff, opt_result, opt_value_result, simple_error, success, XResult};
use serde_json::{json, Map, Value};
use crate::jose; use crate::jose;
use crate::jose::jwk_to_rsa_pubic_key;
pub struct CommandImpl; pub struct CommandImpl;
@@ -17,54 +19,102 @@ impl Command for CommandImpl {
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Local mini KMS cli") SubCommand::with_name(self.name()).about("Local mini KMS cli")
.arg(Arg::with_name("connect").long("connect").takes_value(true).default_value("127.0.0.1:5567").help("Connect server")) .arg(Arg::with_name("connect").long("connect").short("C").takes_value(true).default_value("127.0.0.1:5567").help("Connect server"))
.arg(Arg::with_name("init").long("init").help("Init server")) .arg(Arg::with_name("init").long("init").help("Init server"))
.arg(Arg::with_name("direct-init").long("direct-init").help("Direct init server"))
.arg(Arg::with_name("offline-init").long("offline-init").help("Offline init server")) .arg(Arg::with_name("offline-init").long("offline-init").help("Offline init server"))
.arg(Arg::with_name("encrypt").long("encrypt").help("Encrypt text")) .arg(Arg::with_name("encrypt").long("encrypt").help("Encrypt text"))
.arg(Arg::with_name("decrypt").long("decrypt").help("Decrypt text")) .arg(Arg::with_name("decrypt").long("decrypt").help("Decrypt text"))
.arg(Arg::with_name("value").long("value").takes_value(true).help("Value, for encrypt or decrypt")) .arg(Arg::with_name("read").long("read").help("Read value"))
.arg(Arg::with_name("value-hex").long("value-hex").takes_value(true).help("Value(hex), for encrypt")) .arg(Arg::with_name("write").long("write").help("Write value"))
.arg(Arg::with_name("value-base64").long("value-base64").takes_value(true).help("Value(base64), for encrypt")) .arg(Arg::with_name("name").long("name").short("n").takes_value(true).help("Read/Write key name"))
.arg(Arg::with_name("value").long("value").short("v").takes_value(true).help("Value, for encrypt or decrypt"))
.arg(Arg::with_name("value-hex").long("value-hex").short("x").takes_value(true).help("Value(hex), for encrypt"))
.arg(Arg::with_name("value-base64").long("value-base64").short("b").takes_value(true).help("Value(base64), for encrypt"))
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge"))
.arg(Arg::with_name("comment").long("comment").takes_value(true).help("Comment"))
.arg(Arg::with_name("force-write").long("force-write").short("F").help("Force write value"))
.arg(Arg::with_name("read-from-pinentry").long("read-from-pinentry").help("Read from pin-entry"))
.arg(Arg::with_name("ssh-remote").long("ssh-remote").takes_value(true).help("SSH remote, root@example or localhost"))
} }
fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let init = sub_arg_matches.is_present("init"); let init = sub_arg_matches.is_present("init");
let direct_init = sub_arg_matches.is_present("direct-init");
let offline_init = sub_arg_matches.is_present("offline-init"); let offline_init = sub_arg_matches.is_present("offline-init");
let encrypt = sub_arg_matches.is_present("encrypt"); let encrypt = sub_arg_matches.is_present("encrypt");
let decrypt = sub_arg_matches.is_present("decrypt"); let decrypt = sub_arg_matches.is_present("decrypt");
let read = sub_arg_matches.is_present("read");
let write = sub_arg_matches.is_present("write");
let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error"); let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error");
if init { if init {
rt.block_on(async { rt.block_on(async { do_init(arg_matches, sub_arg_matches).await })
do_init(arg_matches, sub_arg_matches).await } else if direct_init {
}) rt.block_on(async { do_direct_init(arg_matches, sub_arg_matches).await })
} else if offline_init { } else if offline_init {
do_offline_init(arg_matches, sub_arg_matches) do_offline_init(arg_matches, sub_arg_matches)
} else if encrypt { } else if encrypt {
rt.block_on(async { rt.block_on(async { do_encrypt(arg_matches, sub_arg_matches).await })
do_encrypt(arg_matches, sub_arg_matches).await
})
} else if decrypt { } else if decrypt {
rt.block_on(async { rt.block_on(async { do_decrypt(arg_matches, sub_arg_matches).await })
do_decrypt(arg_matches, sub_arg_matches).await } else if read {
}) rt.block_on(async { do_read(arg_matches, sub_arg_matches).await })
} else if write {
rt.block_on(async { do_write(arg_matches, sub_arg_matches).await })
} else { } else {
simple_error!("Need a flag") simple_error!("Need a flag")
} }
} }
} }
async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error"); let value_hex = sub_arg_matches.value_of("value-hex");
let value_base64 = sub_arg_matches.value_of("value-base64");
let yubikey_challenge = sub_arg_matches.value_of("yubikey-challenge");
let mut body_map = Map::new();
if let Some(value_hex) = value_hex {
body_map.insert("clear_master_key_hex".to_string(), value_hex.into());
} else if let Some(value_base64) = value_base64 {
body_map.insert("clear_master_key_base64".to_string(), value_base64.into());
} else {
let pin = match pinentry_util::read_pin(
Some("Input your clear master key, starts with hex: or base64:"),
Some("Clear master key: ")) {
Ok(pin) => pin,
Err(e) => return simple_error!("Read clear master key failed: {}", e),
};
let pin_str = pin.get_pin();
let clear_master_key = if pin_str.starts_with("hex:") {
let hex: String = pin_str.chars().skip(4).collect();
hex::decode(&hex)?
} else if pin_str.starts_with("base64:") {
let base64: String = pin_str.chars().skip(7).collect();
STANDARD.decode(&base64)?
} else {
return simple_error!("Clear master key must starts with hex: or base64:");
};
body_map.insert("clear_master_key_hex".to_string(), hex::encode(&clear_master_key).into());
}
if let Some(yubikey_challenge) = yubikey_challenge {
body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into());
}
let _data = do_inner_request(sub_arg_matches, "init", &Value::Object(body_map)).await?;
success!("Init finished");
Ok(Some(0))
}
async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let ssh_remote = sub_arg_matches.value_of("ssh-remote").map(|s| s.to_string());
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
let client = Client::new();
let uri = format!("http://{}/status", connect); 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() {
@@ -79,34 +129,140 @@ 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 {
let body = if let Some(name) = sub_arg_matches.value_of("name") {
json!({ "name": name })
} else {
return simple_error!("Require key");
};
let data = do_inner_request(sub_arg_matches, "read", &body).await?;
println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0))
}
async fn do_write(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let name = if let Some(name) = sub_arg_matches.value_of("name") {
name
} else {
return simple_error!("Require key");
};
let value = sub_arg_matches.value_of("value");
let value_hex = sub_arg_matches.value_of("value-hex");
let value_base64 = sub_arg_matches.value_of("value-base64");
let force_write = sub_arg_matches.is_present("force-write");
let comment = sub_arg_matches.value_of("comment");
let body = if let Some(value) = value {
json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value": value}) })
} else if let Some(value_hex) = value_hex {
json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value_hex": value_hex}) })
} else if let Some(value_base64) = value_base64 {
json!({ "name": name, "force_write": force_write, "comment": comment, "value": json!({"value_base64": value_base64}) })
} else {
return simple_error!("Require one of value, value-hex, value-base64");
};
let data = do_inner_request(sub_arg_matches, "write", &body).await?;
println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0))
}
async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let value = sub_arg_matches.value_of("value"); let value = sub_arg_matches.value_of("value");
let value_hex = sub_arg_matches.value_of("value-hex"); let value_hex = sub_arg_matches.value_of("value-hex");
let value_base64 = sub_arg_matches.value_of("value-base64"); let value_base64 = sub_arg_matches.value_of("value-base64");
let client = Client::new();
let uri = format!("http://{}/encrypt", connect);
debugging!("Request uri: {}", &uri);
let body = if let Some(value) = value { let body = if let Some(value) = value {
json!({ "value": value }) json!({ "value": value })
} else if let Some(value_hex) = value_hex { } else if let Some(value_hex) = value_hex {
@@ -116,65 +272,71 @@ async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
} else { } else {
return simple_error!("Require one of value, value-hex, value-base64"); return simple_error!("Require one of value, value-hex, value-base64");
}; };
let body = serde_json::to_string(&body)?; let data = do_inner_request(sub_arg_matches, "encrypt", &body).await?;
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; println!("{}", serde_json::to_string_pretty(&data)?);
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data);
}
let data = response_to_value(req_response).await?;
success!("Encrypted value: {}", data["encrypted_value"].as_str().expect("Get encrypted_value error"));
Ok(Some(0)) Ok(Some(0))
} }
async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError { async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let value = opt_value_result!(sub_arg_matches.value_of("value"), "Argument value required"); let value = opt_value_result!(sub_arg_matches.value_of("value"), "Argument value required");
let client = Client::new();
let uri = format!("http://{}/decrypt", connect);
debugging!("Request uri: {}", &uri);
let body = json!({ "encrypted_value": value }); let body = json!({ "encrypted_value": value });
let body = serde_json::to_string(&body)?;
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?; let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?;
if req_response.status() != StatusCode::OK { println!("{}", serde_json::to_string_pretty(&data)?);
let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data);
}
let data = response_to_value(req_response).await?;
success!("Encrypted value(hex): {}", data["value_hex"].as_str().expect("Get value_hex error"));
success!("Encrypted value(base64): {}", data["value_base64"].as_str().expect("Get value_base64 error"));
success!("Encrypted value: {}", String::from_utf8_lossy(&hex::decode(data["value_hex"].as_str().expect("Get value_hex error"))?).to_string());
Ok(Some(0)) 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)?;
base64::decode(&base64)?
} else {
line.as_bytes().to_vec()
};
let jwk = read_line("Input JWK: ")?;
let jwk = Jwk::from_bytes(jwk.as_bytes())?;
let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &jwk)?;
success!("Encrypted master key: {}", encrypted_master_key); 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> {
std::io::stdout().write(prompt.as_bytes()).ok(); 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(); std::io::stdout().flush().ok();
let mut line = String::new(); let mut line = String::new();
if let Err(e) = std::io::stdin().read_line(&mut line) { if let Err(e) = std::io::stdin().read_line(&mut line) {
@@ -183,9 +345,30 @@ 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?;
let data: Value = serde_json::from_reader(whole_body.reader())?; let data: Value = serde_json::from_reader(whole_body.reader())?;
Ok(data) Ok(data)
} }
async fn do_inner_request(sub_arg_matches: &ArgMatches<'_>, action: &str, body: &Value) -> XResult<Value> {
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let body = serde_json::to_string(&body)?;
let client = Client::new();
let uri = format!("http://{}/{}", connect, action);
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?;
// if req_response.status() != StatusCode::OK {
// let status = req_response.status().as_u16();
// let data = response_to_value(req_response).await?;
// return simple_error!("Server status is not success: {}, response: {}", status, data);
// }
response_to_value(req_response).await
}

182
src/db.rs
View File

@@ -1,57 +1,193 @@
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: &'static str = "__master_verification_key"; pub const DEFAULT_MASTER_KEY_VERIFICATION_KEY: &str = "__master_verification_key";
pub struct Key { pub struct Key {
pub name: String, pub name: String,
pub encrypted_key: String, pub encrypted_key: String,
pub comment: Option<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> { 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("SELECT name FROM sqlite_master WHERE type='table' AND name='keys'")?; if let Ok(false) = check_table_keys(conn) {
let mut rows = stmt.query(())?; repair_table_keys(conn)?;
if rows.next()?.is_some() {
information!("Table keys exists, skip init");
return Ok(false);
} }
let _ = conn.execute(r##"
CREATE TABLE keys (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
value TEXT
)
"##, ())?;
success!("Table keys created");
Ok(true) Ok(true)
} }
pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> { 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( let _ = conn.execute(
"INSERT INTO keys (name, value) VALUES (?1, ?2)", r##"
(&key.name, &key.encrypted_key), CREATE TABLE keys (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
value TEXT,
comment TEXT
)"##,
(),
)?; )?;
Ok(()) 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),
),
)?;
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",
(&key.encrypted_key, comment, &key.name),
)?;
} else {
let _ = conn.execute(
"UPDATE keys SET value = ?1 WHERE name = ?2",
(&key.encrypted_key, &key.name),
)?;
}
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 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| {
Ok(Key { Ok(Key {
name: row.get(1)?, name: row.get(1)?,
encrypted_key: row.get(2)?, encrypted_key: row.get(2)?,
comment: row.get(3)?,
}) })
})?; })?;
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

@@ -1,53 +1,202 @@
use josekit::jwe; use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use josekit::jwe::alg::aeskw::AeskwJweAlgorithm; use aes_kw::Kek;
use josekit::jwe::alg::rsaes::RsaesJweAlgorithm; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use josekit::jwe::JweHeader; use base64::Engine;
use josekit::jwk::alg::rsa::RsaKeyPair; use jose_jwk::{Key, Rsa};
use josekit::jwk::Jwk; use rand::{random, thread_rng};
use rust_util::XResult; use rsa::pkcs1::LineEnding;
use serde_json::Value; use rsa::pkcs8::EncodePublicKey;
use sha2::Digest; use rsa::{Oaep, RsaPrivateKey, RsaPublicKey};
use rust_util::{iff, opt_result, simple_error, XResult};
use serde_derive::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256};
const LOCAL_KMS_PREFIX: &'static str = "LKMS:"; const LOCAL_KMS_PREFIX: &str = "LKMS:";
const JWE_ENC_A256GCM: &str = "A256GCM";
const JWE_ALG_A256KW: &str = "A256KW";
const JWE_ALG_RSA_OAEP: &str = "RSA-OAEP";
const JWE_DOT: &str = ".";
pub fn generate_rsa_key(bits: u32) -> XResult<RsaKeyPair> { #[derive(Default, Debug, Serialize, Deserialize)]
Ok(RsaKeyPair::generate(bits)?) 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 serialize_jwe_rsa(payload: &[u8], jwk: &Jwk) -> XResult<String> { pub fn generate_rsa_key(bits: u32) -> XResult<RsaPrivateKey> {
let mut header = JweHeader::new(); let mut rng = thread_rng();
header.set_content_encryption("A256GCM"); Ok(RsaPrivateKey::new(&mut rng, bits as usize)?)
header.set_claim("vendor", Some(Value::String("local-mini-kms".to_string())))?;
let encrypter = RsaesJweAlgorithm::RsaOaep.encrypter_from_jwk(&jwk)?;
Ok(format!("{}{}", LOCAL_KMS_PREFIX, jwe::serialize_compact(payload, &header, &encrypter)?))
} }
pub fn deserialize_jwe_rsa(jwe: &str, jwk: &Jwk) -> XResult<(Vec<u8>, JweHeader)> { pub fn rsa_key_to_jwk(rsa_private_key: &RsaPrivateKey) -> XResult<jose_jwk::Jwk> {
let decrypter = RsaesJweAlgorithm::RsaOaep.decrypter_from_jwk(jwk)?; let rsa_public_key = rsa_private_key.as_ref();
Ok(jwe::deserialize_compact(&get_jwe(jwe), &decrypter)?) let public_rsa: Rsa = rsa_public_key.into();
Ok(jose_jwk::Jwk {
key: Key::Rsa(public_rsa),
prm: Default::default(),
})
}
pub fn rsa_key_to_pem(rsa_private_key: &RsaPrivateKey) -> XResult<String> {
Ok(rsa_private_key.to_public_key().to_public_key_pem(LineEnding::LF)?)
}
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_public_key = opt_result!(RsaPublicKey::try_from(rsa), "Bad RSA JWK: {}, error: {:?}", rsa_jwk);
Ok(rsa_public_key)
}
pub fn serialize_jwe_rsa(payload: &[u8], rsa_public_key: &RsaPublicKey) -> XResult<String> {
let header = JweHeader {
enc: JWE_ENC_A256GCM.to_string(),
alg: JWE_ALG_RSA_OAEP.to_string(),
vendor: "local-mini-kms".to_string(),
..Default::default()
};
serialize_jwe_fn(&header, payload, |data_key| -> XResult<Vec<u8>> {
let mut r = thread_rng();
Ok(opt_result!(rsa_public_key.encrypt(&mut r, Oaep::new::<Sha1>(), data_key), "Wrap key failed: {}"))
})
}
pub fn deserialize_jwe_rsa(jwe: &str, rsa: &RsaPrivateKey) -> XResult<(Vec<u8>, JweHeader)> {
deserialize_jwe_fn(jwe, |alg, key_wrap| -> 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: {}"))
})
} }
pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult<String> { pub fn serialize_jwe_aes(payload: &[u8], key: &[u8]) -> XResult<String> {
let mut header = JweHeader::new(); serialize_jwe_aes_32(None, None, payload, to_bytes32(key)?)
header.set_content_encryption("A256GCM"); }
header.set_claim("vendor", Some(Value::String("local-mini-kms".to_string())))?;
header.set_claim("version", Some(Value::String(get_master_key_checksum(key))))?; pub fn serialize_jwe_aes_with_data_type(data_type: &str, exportable: bool, payload: &[u8], key: &[u8]) -> XResult<String> {
let encrypter = AeskwJweAlgorithm::A256kw.encrypter_from_bytes(key)?; serialize_jwe_aes_32(Some(data_type.to_string()), iff!(exportable, None, Some(false)), payload, to_bytes32(key)?)
Ok(format!("{}{}", LOCAL_KMS_PREFIX, jwe::serialize_compact(payload, &header, &encrypter)?)) }
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: {}"))
})
} }
pub fn deserialize_jwe_aes(jwe: &str, key: &[u8]) -> XResult<(Vec<u8>, JweHeader)> { pub fn deserialize_jwe_aes(jwe: &str, key: &[u8]) -> XResult<(Vec<u8>, JweHeader)> {
let decrypter = AeskwJweAlgorithm::A256kw.decrypter_from_bytes(key)?; deserialize_jwe_aes_32(jwe, to_bytes32(key)?)
Ok(jwe::deserialize_compact(&get_jwe(jwe), &decrypter)?) }
pub fn deserialize_jwe_aes_32(jwe: &str, key: [u8; 32]) -> XResult<(Vec<u8>, JweHeader)> {
deserialize_jwe_fn(jwe, |alg, key_wrap| -> XResult<Vec<u8>> {
if alg != JWE_ALG_A256KW {
return simple_error!("Invalid JWE header alg: {}", alg);
}
let kek = Kek::from(key);
Ok(opt_result!(kek.unwrap_vec(key_wrap), "Unwrap key failed: {}"))
})
}
fn serialize_jwe_fn<F>(header: &JweHeader, payload: &[u8], key_wrap_fn: F) -> XResult<String>
where
F: Fn(&[u8]) -> XResult<Vec<u8>>,
{
let header_str = opt_result!(serde_json::to_string(&header), "Invalid JWE header: {}");
let header_b64 = URL_SAFE_NO_PAD.encode(header_str.as_bytes());
let data_key: [u8; 32] = random();
let iv: [u8; 12] = random();
let mut encryptor = Aes256GcmStreamEncryptor::new(data_key, &iv);
encryptor.init_adata(header_b64.as_bytes());
let mut ciphertext = encryptor.update(payload);
let (ciphertext_final, tag) = encryptor.finalize();
ciphertext.extend_from_slice(&ciphertext_final);
let cek = key_wrap_fn(&data_key)?;
Ok(format!(
"{}{}.{}.{}.{}.{}",
LOCAL_KMS_PREFIX,
header_b64,
URL_SAFE_NO_PAD.encode(&cek),
URL_SAFE_NO_PAD.encode(iv),
URL_SAFE_NO_PAD.encode(&ciphertext),
URL_SAFE_NO_PAD.encode(&tag)
))
}
fn deserialize_jwe_fn<F>(jwe: &str, key_unwrap_fn: F) -> XResult<(Vec<u8>, JweHeader)>
where
F: Fn(&str, &[u8]) -> XResult<Vec<u8>>,
{
let jwe = get_jwe(jwe);
let jwe_parts = jwe.split(JWE_DOT).collect::<Vec<&str>>();
if jwe_parts.len() != 5 {
return simple_error!("Invalid JWE: {}", jwe);
}
let header_bytes = opt_result!(decode_url_safe_no_pad(jwe_parts[0]), "Invalid JWE header: {}, JWE: {}", jwe);
let header: JweHeader = opt_result!(serde_json::from_slice(&header_bytes), "Invalid JWE header: {}, JWE: {}", jwe);
if header.enc != JWE_ENC_A256GCM {
return simple_error!("Invalid JWE header enc: {}", header.enc);
}
let cek = opt_result!(decode_url_safe_no_pad(jwe_parts[1]), "Invalid JWE CEK: {}, JWE: {}", jwe);
let iv = opt_result!(decode_url_safe_no_pad(jwe_parts[2]), "Invalid JWE IV: {}, JWE: {}", jwe);
let ciphertext = opt_result!(decode_url_safe_no_pad(jwe_parts[3]), "Invalid JWE ciphertext: {}, JWE: {}", jwe);
let tag = opt_result!(decode_url_safe_no_pad(jwe_parts[4]), "Invalid JWE tag: {}, JWE: {}", jwe);
let data_key = key_unwrap_fn(&header.alg, &cek)?;
let data_key_b32 = opt_result!(to_bytes32(&data_key), "Invalid JWE CEK: {}, JWE: {}", jwe);
let mut decryptor = Aes256GcmStreamDecryptor::new(data_key_b32, &iv);
decryptor.init_adata(jwe_parts[0].as_bytes());
let mut plaintext = decryptor.update(&ciphertext);
let plaintext_2 = decryptor.update(&tag);
let plaintext_final = opt_result!(decryptor.finalize(), "Invalid JWE: {}, JWE: {}", jwe);
plaintext.extend_from_slice(&plaintext_2);
plaintext.extend_from_slice(&plaintext_final);
Ok((plaintext, header))
}
#[inline]
fn decode_url_safe_no_pad(s: &str) -> XResult<Vec<u8>> {
Ok(URL_SAFE_NO_PAD.decode(s.as_bytes())?)
}
#[inline]
fn to_bytes32(bytes: &[u8]) -> XResult<[u8; 32]> {
if bytes.len() != 32 {
return simple_error!("Not valid 32 bytes");
}
let mut ret = [0; 32];
ret.copy_from_slice(&bytes[..32]);
Ok(ret)
} }
fn get_master_key_checksum(key: &[u8]) -> String { fn get_master_key_checksum(key: &[u8]) -> String {
let digest = sha2::Sha256::digest(&key); let digest = Sha256::digest(key);
let digest = sha2::Sha256::digest(&digest.as_slice()); let digest = Sha256::digest(digest.as_slice());
let digest = sha2::Sha256::digest(&digest.as_slice()); let digest = Sha256::digest(digest.as_slice());
let digest = sha2::Sha256::digest(&digest.as_slice()); let digest = Sha256::digest(digest.as_slice());
let digest = sha2::Sha256::digest(&digest.as_slice()); let digest = Sha256::digest(digest.as_slice());
let digest = sha2::Sha256::digest(&digest.as_slice()); let digest = Sha256::digest(digest.as_slice());
hex::encode(&digest[0..8]) hex::encode(&digest[0..8])
} }
@@ -58,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

@@ -1,11 +1,28 @@
use clap::{App, AppSettings, ArgMatches}; use clap::{App, AppSettings, ArgMatches};
use rust_util::{failure_and_exit, information};
use rust_util::util_clap::{Command, CommandError}; 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 db;
mod proc;
mod jose; mod jose;
mod cli; mod cli;
mod serve; mod serve;
mod serve_common;
mod serve_status;
mod serve_init;
mod serve_encrypt_decrypt;
mod serve_read_write;
#[cfg(feature = "yubikey")]
mod yubikey_hmac;
#[cfg(feature = "yubikey")]
mod yubikey_init_master_key;
mod serve_datakey;
mod serve_log;
pub struct DefaultCommandImpl; pub struct DefaultCommandImpl;
@@ -25,15 +42,40 @@ 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);
match secmem_proc::harden_process() {
Err(e) => if ignore_harden_process_error {
warning!("Harden local-mini-kms failed: {}", e);
} else {
failure_and_exit!("Harden local-mini-kms failed: {}", e);
}
Ok(_) => success!("Harden local-mini-kms success"),
}
}
fn inner_main() -> CommandError { fn inner_main() -> CommandError {
let commands: Vec<Box<dyn Command>> = vec![ let commands: Vec<Box<dyn Command>> = vec![
Box::new(cli::CommandImpl), Box::new(cli::CommandImpl),
Box::new(serve::CommandImpl), Box::new(serve::CommandImpl),
#[cfg(feature = "yubikey")]
Box::new(yubikey_init_master_key::CommandImpl),
]; ];
let mut features: Vec<String> = vec![];
#[allow(clippy::vec_init_then_push)]
{
#[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")) let mut app = App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION")) .about(env!("CARGO_PKG_DESCRIPTION"))
.long_about("Local mini KMS") .long_about(long_about.as_str())
.setting(AppSettings::ColoredHelp); .setting(AppSettings::ColoredHelp);
app = DefaultCommandImpl::process_command(app); app = DefaultCommandImpl::process_command(app);
for command in &commands { for command in &commands {
@@ -42,6 +84,8 @@ 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(); }
return command.run(&matches, sub_cmd_matches); return command.run(&matches, sub_cmd_matches);
} }
} }

62
src/proc.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Process {
pub pid: i32,
pub comm: String,
pub exec: Option<PathBuf>,
}
#[cfg(not(all(target_os = "linux", feature = "harden_process")))]
pub fn get_process(_port: u16) -> Option<Process> {
None
}
#[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) => {
rust_util::warning!("Get procfs all processes failed: {}", e);
return None;
}
Ok(all_procs) => all_procs,
};
let mut map = std::collections::HashMap::new();
for p in all_procs {
if let Ok(process) = p {
if let (Ok(stat), Ok(fds)) = (process.stat(), process.fd()) {
for fd in fds {
if let Ok(fd) = fd {
if let procfs::process::FDTarget::Socket(inode) = fd.target {
map.insert(inode, (stat.clone(), process.exe().ok()));
}
}
}
}
}
}
let tcp = match procfs::net::tcp() {
Err(e) => {
rust_util::warning!("Get procfs net tcp failed: {}", e);
return None;
}
Ok(tcp) => tcp,
};
let local_ip = std::net::Ipv4Addr::LOCALHOST;
for entry in tcp.into_iter() {
if local_ip == entry.local_address.ip() && port == entry.local_address.port() {
if let Some((stat, exec)) = map.get(&entry.inode) {
return Some(Process {
pid: stat.pid,
comm: stat.comm.clone(),
exec: exec.clone(),
});
} else {
rust_util::warning!("Cannot get process by port: {}, inode: {}", port, entry.inode);
return None;
}
}
}
rust_util::warning!("Port not found: {}", port);
None
}

View File

@@ -1,59 +1,125 @@
use std::sync::RwLock;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use hyper::{Body, Client, Method, Request, Response, Server, StatusCode};
use hyper::body::Buf;
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn}; use hyper::service::{make_service_fn, service_fn};
use josekit::jwk::alg::rsa::RsaKeyPair; use hyper::{Body, Client, Method, Request, Response, Server, StatusCode};
use josekit::jwk::KeyPair;
use rust_util::{debugging, failure_and_exit, information, opt_result, simple_error, success, XResult};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use serde::{Deserialize, Serialize}; use rust_util::{failure_and_exit, information, success, warning, XResult};
use serde_json::{json, Map, Value}; use serde_json::{json, Value};
use zeroize::Zeroize; use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::runtime::Runtime;
use crate::{db, jose}; use crate::serve_common::{self, GenericError, MemoryKey, Result};
use crate::db::Key; use crate::serve_init;
use crate::serve_init::InitRequest;
type GenericError = Box<dyn std::error::Error + Send + Sync>; use crate::serve_read_write;
type Result<T> = std::result::Result<T, GenericError>; use crate::serve_status;
#[cfg(feature = "yubikey")]
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; 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").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").takes_value(true).default_value("local-mini-kms.db").help("Local db file")) .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 { 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"),
Err(e) => failure_and_exit!("Init server failed: {}", e), Err(e) => failure_and_exit!("Init server failed: {}", e),
} }
let listen = sub_arg_matches.value_of("listen").expect("Get argument listen error"); let rt = Runtime::new().expect("Create tokio runtime error");
let rt = tokio::runtime::Runtime::new().expect("Create tokio runtime error"); #[cfg(feature = "yubikey")]
init_with_yubikey_challenge(&rt, sub_arg_matches);
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().expect(&format!("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 |_| { let new_service = make_service_fn(move |conn: &AddrStream| {
let remote_addr = conn.remote_addr();
let client = client.clone(); let client = client.clone();
async { async move {
Ok::<_, GenericError>(service_fn(move |req| { Ok::<_, GenericError>(service_fn(move |req| {
response_requests(req, client.to_owned()) response_requests(remote_addr, req, client.to_owned())
})) }))
} }
}); });
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"),
@@ -66,49 +132,63 @@ impl Command for CommandImpl {
// ref: https://github.com/hyperium/hyper/blob/master/examples/web_api.rs // ref: https://github.com/hyperium/hyper/blob/master/examples/web_api.rs
// ref: https://crates.io/crates/rusqlite // ref: https://crates.io/crates/rusqlite
async fn response_requests( async fn response_requests(
remote_addr: SocketAddr,
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());
match 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,
process.comm,
process.exec
),
}
match (req.method(), req.uri().path()) { match (req.method(), req.uri().path()) {
(&Method::POST, "/init") => init(req).await, (&Method::POST, "/init") => serve_init::init(req).await,
(&Method::POST, "/update") => update().await, (&Method::POST, "/update") => update().await,
(&Method::POST, "/decrypt") => decrypt(req).await, (&Method::POST, "/decrypt") => serve_encrypt_decrypt::decrypt(req).await,
(&Method::POST, "/encrypt") => encrypt(req).await, (&Method::POST, "/encrypt") => serve_encrypt_decrypt::encrypt(req).await,
(&Method::GET, "/status") => status().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, "/version") => get_version().await,
_ => Ok(Response::builder().status(StatusCode::NOT_FOUND).body(serde_json::to_string_pretty(&json!({ "error": "not_found" }))?.into())?), (&Method::GET, "/") => get_root().await,
} _ => Ok(Response::builder().status(StatusCode::NOT_FOUND).body(
} format!(
"{}\n",
macro_rules! do_response { serde_json::to_string_pretty(&json!({
($ex: expr) => ( "error": "not_found",
match $ex { }))?
Ok((status_code, body)) => Ok(Response::builder().status(status_code).body(serde_json::to_string_pretty(&body)?.into())?),
Err(e) => Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(serde_json::to_string_pretty(&json!({
"error": "internal_error",
"error_message": format!("{}", e),
}))?.into())?),
}
) )
.into(),
)?),
}
} }
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
struct MemoryKey {
database_file: String,
instance_rsa_key_pair: RsaKeyPair,
master_key: Option<Vec<u8>>,
}
lazy_static::lazy_static! {
static ref STATUP_RW_LOCK: RwLock<Option<MemoryKey>> = RwLock::new(None);
}
fn init_instance(db: &str) -> XResult<bool> { 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 = STATUP_RW_LOCK.write().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 => {
@@ -124,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 = STATUP_RW_LOCK.write().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)?;
@@ -134,207 +216,108 @@ fn update_instance_rsa_key_pair() -> XResult<bool> {
} }
} }
#[derive(Serialize, Deserialize)]
struct MultipleViewValue {
value: Option<String>,
value_hex: Option<String>,
value_base64: Option<String>,
}
impl MultipleViewValue {
fn from(v: &[u8]) -> Self {
Self {
value: Some(String::from_utf8_lossy(v).to_string()),
value_hex: Some(hex::encode(v)),
value_base64: Some(base64::encode(v)),
}
}
fn to_bytes(&self) -> XResult<Vec<u8>> {
if let Some(v) = &self.value {
Ok(v.as_bytes().to_vec())
} else if let Some(v) = &self.value_hex {
let v = opt_result!(hex::decode(v), "Decode hex failed: {}");
Ok(v)
} else if let Some(v) = &self.value_base64 {
let v = opt_result!(base64::decode(v), "Decode base64 failed: {}");
Ok(v)
} else {
simple_error!("Multiple view value is all empty")
}
}
}
#[derive(Serialize, Deserialize)]
struct DecryptRequest {
encrypted_value: String,
}
async fn decrypt(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_decrypt(req).await)
}
async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?;
let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?;
debugging!("To be decrypted value: {}", &data.encrypted_value);
let mut key = match get_master_key() {
None => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "status_not_ready" }))),
Some(key) => key,
};
let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key);
key.zeroize();
decrypted_value.map(|v| {
let v = MultipleViewValue::from(&v.0);
let mut map = Map::new();
if let Some(v) = &v.value {
map.insert("value".to_string(), Value::String(v.to_string()));
}
if let Some(v) = &v.value_hex {
map.insert("value_hex".to_string(), Value::String(v.to_string()));
}
if let Some(v) = &v.value_base64 {
map.insert("value_base64".to_string(), Value::String(v.to_string()));
}
(StatusCode::OK, Value::Object(map))
})
}
async fn encrypt(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_encrypt(req).await)
}
async fn inner_encrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?;
let data: MultipleViewValue = serde_json::from_reader(whole_body.reader())?;
let value = data.to_bytes()?;
let mut key = match get_master_key() {
None => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "status_not_ready" }))),
Some(key) => key,
};
let encrypt_result = jose::serialize_jwe_aes(&value, &key);
key.zeroize();
encrypt_result.map(|e| {
(StatusCode::OK, json!({
"encrypted_value": e,
}))
})
}
async fn update() -> Result<Response<Body>> { async fn update() -> Result<Response<Body>> {
do_response!(inner_update().await) do_response!(inner_update().await)
} }
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 init(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_init(req).await)
}
#[derive(Serialize, Deserialize)]
struct InitRequest {
clear_master_key_hex: Option<String>,
clear_master_key_base64: Option<String>,
encrypted_master_key: Option<String>,
}
async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?;
let init_request: InitRequest = serde_json::from_reader(whole_body.reader())?;
let mut startup_rw_lock = STATUP_RW_LOCK.write().expect("Lock read startup rw lock error");
match &*startup_rw_lock {
None => return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))),
Some(memory_key) => match memory_key.master_key {
Some(_) => return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "already init " }))),
None => {}
},
}
let clear_master_key = if let Some(clear_master_key_base64) = &init_request.clear_master_key_base64 {
base64::decode(clear_master_key_base64)?
} else if let Some(clear_master_key_hex) = init_request.clear_master_key_hex {
hex::decode(clear_master_key_hex)?
} else if let Some(encrypted_master_key) = init_request.encrypted_master_key {
debugging!("Received encrypted master key: {}", encrypted_master_key);
if let Some(k) = &*startup_rw_lock {
let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair.to_jwk_private_key())?;
clear_master_key
} else {
return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " })));
}
} else {
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "master key is not assigned" })));
};
if clear_master_key.len() != 32 {
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "bad clear_master_key length" })));
}
if let Some(k) = &mut *startup_rw_lock {
let conn = opt_result!(db::open_db(&k.database_file), "Open db failed: {}");
let default_master_key_verification_key = db::find_key(&conn, db::DEFAULT_MASTER_KEY_VERIFICATION_KEY)?;
match default_master_key_verification_key {
None => {
let key = Key {
name: db::DEFAULT_MASTER_KEY_VERIFICATION_KEY.to_string(),
encrypted_key: jose::serialize_jwe_aes("LOCAL-MINI-KMS:MAGIC-VERIFICATION-KEY".as_bytes(), &clear_master_key)?,
};
db::insert_key(&conn, &key)?;
}
Some(key) => {
debugging!("Found jwe: {}", &key.encrypted_key);
let _ = opt_result!(jose::deserialize_jwe_aes(&key.encrypted_key, &clear_master_key), "Deserialize master key verification key failed: {}");
}
}
information!("Set master key success");
k.master_key = Some(clear_master_key);
k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?;
}
Ok((StatusCode::OK, json!({})))
}
async fn status() -> Result<Response<Body>> {
do_response!(inner_status().await)
}
async fn inner_status() -> XResult<(StatusCode, Value)> {
let startup_rw_lock = STATUP_RW_LOCK.read().expect("Lock read startup rw lock error");
let body = match &*startup_rw_lock {
None => json!({ "status": "n/a" }),
Some(memory_key) => match memory_key.master_key {
None => json!({
"status": "not-ready",
"instance_public_key_jwk": memory_key.instance_rsa_key_pair.to_jwk_key_pair().to_public_key()?,
"instance_public_key_pem": String::from_utf8_lossy(&memory_key.instance_rsa_key_pair.to_pem_public_key()).to_string(),
}), }),
Some(_) => json!({ ))
"status": "ready",
}),
}
};
Ok((StatusCode::OK, body))
} }
async fn get_version() -> Result<Response<Body>> { 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(),
)?)
} }
fn get_master_key() -> Option<Vec<u8>> { async fn get_root() -> Result<Response<Body>> {
let startup_rw_lock = STATUP_RW_LOCK.read().expect("Lock read startup rw lock error"); if std::env::var("LOCAL_MINI_KMS_HELP").is_ok() {
match &*startup_rw_lock { Ok(Response::builder().body(
None => None, format!(
Some(k) => k.master_key.clone(), r##"{} - {}
Supports commands:
- GET /version
- GET /status
- POST /init
- POST /update
- POST /encrypt
- POST /decrypt
- POST /read
- POST /write
"##,
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
)
.into(),
)?)
} else {
Ok(Response::builder().body("Root Not Found\n".into())?)
}
}
#[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 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 =
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;
}
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,
};
match jose::deserialize_jwe_aes(init_encrypted_master_key, &challenge_key) {
Err(e) => warning!("Yubikey seal master key failed: {}", e),
Ok((key, _)) => {
success!("Yubikey un-seal master key success");
let init_master_key_result = rt.block_on(async {
serve_init::inner_init_request(InitRequest {
yubikey_challenge: None,
clear_master_key_hex: Some(hex::encode(&key)),
clear_master_key_base64: None,
encrypted_master_key: None,
})
.await
});
match init_master_key_result {
Err(e) => warning!("Init master key failed: {}", e),
Ok((StatusCode::OK, _)) => success!("Init master key success"),
Ok((_, response)) => warning!("Init master failed: {}", response),
}
}
} }
} }

178
src/serve_common.rs Normal file
View File

@@ -0,0 +1,178 @@
use std::io::{Cursor, Read};
use std::sync::Mutex;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
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::{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) => (
match $ex {
Ok((status_code, body)) => Ok(Response::builder().status(status_code).body((serde_json::to_string_pretty(&body)? + "\n").into())?),
Err(e) => Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(
format!("{}\n", serde_json::to_string_pretty(&json!({
"error": "internal_error",
"error_message": format!("{}", e),
}))?).into()
)?),
}
)
}
pub fn ok(body: Value) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::OK, body))
}
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)> {
// Ok((
// StatusCode::BAD_REQUEST,
// json!({ "error": error, "error_message": error_message })
// ))
// }
pub struct MemoryKey {
pub database_file: String,
pub instance_rsa_key_pair: RsaPrivateKey,
pub master_key: Option<SecBytes>,
}
lazy_static::lazy_static! {
pub static ref STATUP_RW_LOCK: Mutex<Option<MemoryKey>> = Mutex::new(None);
}
#[derive(Serialize, Deserialize)]
pub struct MultipleViewValue {
pub value: Option<String>,
pub value_hex: Option<String>,
pub value_base64: Option<String>,
}
impl MultipleViewValue {
pub fn from(v: &[u8]) -> Self {
Self {
value: Some(String::from_utf8_lossy(v).to_string()),
value_hex: Some(hex::encode(v)),
value_base64: Some(STANDARD.encode(v)),
}
}
pub fn from_without_value(v: &[u8]) -> Self {
Self {
value: None,
value_hex: Some(hex::encode(v)),
value_base64: Some(STANDARD.encode(v)),
}
}
pub fn to_bytes(&self) -> XResult<Vec<u8>> {
if let Some(v) = &self.value {
Ok(v.as_bytes().to_vec())
} else if let Some(v) = &self.value_hex {
let v = opt_result!(hex::decode(v), "Decode hex failed: {}");
Ok(v)
} else if let Some(v) = &self.value_base64 {
let v = opt_result!(STANDARD.decode(v), "Decode base64 failed: {}");
Ok(v)
} else {
simple_error!("Multiple view value is all empty")
}
}
}
#[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");
match &*startup_rw_lock {
None => None,
Some(k) => match &k.master_key {
None => None,
Some(k) => {
let k = &*k.read();
Some(SecBytes::with(k.len(), |buf| buf.copy_from_slice(k)))
}
},
}
}
pub fn byte_to_multi_view_map(bytes: &[u8], with_value: bool) -> Map<String, Value> {
let v = if with_value {
MultipleViewValue::from(bytes)
} else {
MultipleViewValue::from_without_value(bytes)
};
let mut map = Map::new();
if let Some(v) = &v.value {
map.insert("value".to_string(), Value::String(v.to_string()));
}
if let Some(v) = &v.value_hex {
map.insert("value_hex".to_string(), Value::String(v.to_string()));
}
if let Some(v) = &v.value_base64 {
map.insert("value_base64".to_string(), Value::String(v.to_string()));
}
map
}
pub fn open_local_db() -> XResult<Connection> {
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 initiated!"),
Some(k) => Ok(opt_result!(
db::open_db(&k.database_file),
"Open db failed: {}"
)),
}
}

88
src/serve_datakey.rs Normal file
View File

@@ -0,0 +1,88 @@
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, Request, Response, StatusCode};
use rand::random;
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 {
r#type: String,
spec: String,
name: Option<String>,
comment: Option<String>,
exportable: Option<bool>,
return_plaintext: Option<bool>,
}
pub async fn generate(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_generate(req).await)
}
async fn inner_generate(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let request: DataKeyRequest = parse_request(req).await?;
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.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::client_error("invalid key_type or key_spec"),
};
match response_result {
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.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.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(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_with_data_type(data_key_type, exportable, value, &key.read())?;
Ok((key_plaintext, key_ciphertext))
}

View File

@@ -0,0 +1,64 @@
use hyper::{Body, Request, Response, StatusCode};
use rust_util::XResult;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::jose;
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 {
encrypted_value: String,
}
pub async fn decrypt(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_decrypt(req).await)
}
async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let data: DecryptRequest = parse_request(req).await?;
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?;
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>> {
do_response!(inner_encrypt(req).await)
}
async fn inner_encrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let data: MultipleViewValue = parse_request(req).await?;
let value = data.to_bytes()?;
let key = require_master_key!();
let encrypt_result = jose::serialize_jwe_aes(&value, &key.read());
drop(key);
encrypt_result.map(|e| {
(StatusCode::OK, json!({
"encrypted_value": e,
}))
})
}

100
src/serve_init.rs Normal file
View File

@@ -0,0 +1,100 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use hyper::{Body, Request, Response, StatusCode};
use rust_util::{opt_result, XResult};
use seckey::SecBytes;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use zeroize::Zeroize;
use crate::db::Key;
use crate::do_response;
use crate::serve_common::{self, parse_request, Result};
#[cfg(feature = "yubikey")]
use crate::yubikey_hmac;
use crate::{db, jose};
pub async fn init(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_init(req).await)
}
#[derive(Serialize, Deserialize)]
pub struct InitRequest {
pub yubikey_challenge: Option<String>,
pub clear_master_key_hex: Option<String>,
pub clear_master_key_base64: Option<String>,
pub encrypted_master_key: Option<String>,
}
async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
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 serve_common::server_error("instant_key_pair_not_initialized"),
Some(memory_key) => if memory_key.master_key.is_some() {
return serve_common::client_error("already_initialized");
},
}
let clear_master_key = if let Some(clear_master_key_base64) = &init_request.clear_master_key_base64 {
STANDARD.decode(clear_master_key_base64)?
} else if let Some(clear_master_key_hex) = init_request.clear_master_key_hex {
hex::decode(clear_master_key_hex)?
} else if let Some(encrypted_master_key) = init_request.encrypted_master_key {
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 serve_common::server_error("instant_key_pair_not_initialized");
}
} else {
return serve_common::client_error("master_key_missing");
};
if clear_master_key.len() != 32 {
return serve_common::client_error("bad_master_key_length");
}
if let Some(k) = &mut *startup_rw_lock {
let conn = opt_result!(db::open_db(&k.database_file), "Open db failed: {}");
let default_master_key_verification_key = db::find_key(&conn, db::DEFAULT_MASTER_KEY_VERIFICATION_KEY)?;
match default_master_key_verification_key {
None => {
let key = Key {
name: db::DEFAULT_MASTER_KEY_VERIFICATION_KEY.to_string(),
encrypted_key: jose::serialize_jwe_aes("LOCAL-MINI-KMS:MAGIC-VERIFICATION-KEY".as_bytes(), &clear_master_key)?,
comment: Some("local-mini-kms re-init verification".to_string()),
};
db::insert_key(&conn, &key)?;
}
Some(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: {}");
}
}
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) => log::warn!("Yubikey challenge failed: {}", e),
Ok(challenge_key) => match jose::serialize_jwe_aes(&clear_master_key, &challenge_key) {
Err(e) => log::warn!("Yubikey seal master key failed: {}", e),
Ok(jwe) => log::info!("Yubikey sealed master key: {}", jwe)
},
}
}
let sec_bytes = SecBytes::with(clear_master_key.len(), |buf| buf.copy_from_slice(clear_master_key.as_slice()));
let mut clear_master_key = clear_master_key;
clear_master_key.zeroize();
k.master_key = Some(sec_bytes);
k.instance_rsa_key_pair = jose::generate_rsa_key(4096)?;
}
serve_common::ok(json!({}))
}

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");
}
})
}

135
src/serve_read_write.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::db::Key;
use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, parse_request, MultipleViewValue, Result};
use crate::{db, jose};
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, Map, Value};
#[derive(Serialize, Deserialize)]
struct KeysQuery {
r#type: Option<String>,
name: Option<String>,
limit: Option<usize>,
}
#[derive(Serialize, Deserialize)]
struct Named {
name: String,
}
#[derive(Serialize, Deserialize)]
struct NamedValue {
name: String,
force_write: Option<bool>,
value: MultipleViewValue,
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 named: Named = parse_request(req).await?;
let name = &named.name;
if name.is_empty() {
return serve_common::client_error("name_is_empty");
}
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::client_error("name_not_exists"),
Some(k) => k,
};
let key = require_master_key!();
let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?;
drop(key);
let mut map = byte_to_multi_view_map(&data.0, true);
map.insert("name".to_string(), name.as_str().into());
map.insert("comment".to_string(), db_key_value.comment.into());
serve_common::ok(Value::Object(map))
}
pub async fn write(req: Request<Body>) -> Result<Response<Body>> {
do_response!(inner_write(req).await)
}
async fn inner_write(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let named_value: NamedValue = parse_request(req).await?;
let name = &named_value.name;
if name.is_empty() {
return serve_common::client_error("name_is_empty");
}
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::client_error("name_exists");
}
let value = named_value.value.to_bytes()?;
let key = require_master_key!();
let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?;
drop(key);
let new_db_key = Key {
name: db_key_name,
encrypted_key: encrypt_value.clone(),
comment: named_value.comment,
};
let response_body = if let Some(db_key) = db_key {
db::update_key(&conn, &new_db_key)?;
json!({
"name": name.to_string(),
"override": true,
"encrypted_value": encrypt_value,
"previous_encrypted_value": db_key.encrypted_key,
})
} else {
db::insert_key(&conn, &new_db_key)?;
json!({
"name": name.to_string(),
"override": false,
"encrypted_value": encrypt_value,
})
};
serve_common::ok(response_body)
}

29
src/serve_status.rs Normal file
View File

@@ -0,0 +1,29 @@
use hyper::{Body, Response, StatusCode};
use rust_util::XResult;
use serde_json::{json, Value};
use crate::do_response;
use crate::jose::{rsa_key_to_jwk, rsa_key_to_pem};
use crate::serve_common::{self, Result};
pub async fn status() -> Result<Response<Body>> {
do_response!(inner_status().await)
}
async fn inner_status() -> XResult<(StatusCode, Value)> {
let startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
let body = match &*startup_rw_lock {
None => json!({ "status": "n/a" }),
Some(memory_key) => match memory_key.master_key {
None => json!({
"status": "not-ready",
"instance_public_key_jwk": rsa_key_to_jwk(&memory_key.instance_rsa_key_pair)?,
"instance_public_key_pem": rsa_key_to_pem(&memory_key.instance_rsa_key_pair)?,
}),
Some(_) => json!({
"status": "ready",
}),
}
};
serve_common::ok(body)
}

29
src/yubikey_hmac.rs Normal file
View File

@@ -0,0 +1,29 @@
use std::ops::Deref;
use rust_util::{opt_result, success, XResult};
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::Yubico;
pub fn yubikey_challenge_as_32_bytes(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new();
let device = opt_result!(yubi.find_yubikey(), "Find yubikey failed: {}");
success!("Found key, Vendor ID: {:?}, Product ID: {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
// In HMAC Mode, the result will always be the SAME for the SAME provided challenge
let hmac_result = opt_result!(yubi.challenge_response_hmac(challenge_bytes, config), "Challenge HMAC failed: {}");
// Just for debug, lets check the hex
let v: &[u8] = hmac_result.deref();
let mut r = vec![];
r.extend_from_slice(v);
r.extend_from_slice(&v[0..12]);
Ok(r)
}

View File

@@ -0,0 +1,99 @@
use crate::{jose, yubikey_hmac};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use clap::{App, Arg, ArgMatches, SubCommand};
use rand::random;
use rust_util::util_clap::{Command, CommandError};
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 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"),
)
}
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 hex_value_opt = sub_arg_matches.value_of("key-hex");
let base64_value_opt = sub_arg_matches.value_of("key-base64");
let generate_key_present = sub_arg_matches.is_present("generate-key");
if hex_value_opt.is_none() && base64_value_opt.is_none() && !generate_key_present {
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: {}")
} else if let Some(base64_value) = base64_value_opt {
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)
);
clear_master_key.to_vec()
};
if clear_master_key.len() != 32 {
failure_and_exit!("Master key must be 32 bytes");
}
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: {}"
);
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))
}
}