Compare commits

...

28 Commits

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

1162
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
use hyper::body::Buf;
use hyper::{Body, Client, Method, Request, Response, StatusCode};
use rust_util::util_clap::{Command, CommandError};
use rust_util::{debugging, opt_value_result, simple_error, success, XResult};
use rust_util::{debugging, iff, opt_result, opt_value_result, simple_error, success, XResult};
use serde_json::{json, Map, Value};
use crate::jose;
@@ -34,6 +34,8 @@ impl Command for CommandImpl {
.arg(Arg::with_name("yubikey-challenge").long("yubikey-challenge").short("c").takes_value(true).help("Yubikey challenge"))
.arg(Arg::with_name("comment").long("comment").takes_value(true).help("Comment"))
.arg(Arg::with_name("force-write").long("force-write").short("F").help("Force write value"))
.arg(Arg::with_name("read-from-pinentry").long("read-from-pinentry").help("Read from pin-entry"))
.arg(Arg::with_name("ssh-remote").long("ssh-remote").takes_value(true).help("SSH remote, root@example or localhost"))
}
fn run(&self, arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -76,7 +78,24 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc
} else if let Some(value_base64) = value_base64 {
body_map.insert("clear_master_key_base64".to_string(), value_base64.into());
} else {
return simple_error!("Requires value hex or value base64");
let pin = match pinentry_util::read_pin(
Some("Input your clear master key, starts with hex: or base64:"),
Some("Clear master key: ")) {
Ok(pin) => pin,
Err(e) => return simple_error!("Read clear master key failed: {}", e),
};
let pin_str = pin.get_pin();
let clear_master_key = if pin_str.starts_with("hex:") {
let hex: String = pin_str.chars().skip(4).collect();
hex::decode(&hex)?
} else if pin_str.starts_with("base64:") {
let base64: String = pin_str.chars().skip(7).collect();
STANDARD.decode(&base64)?
} else {
return simple_error!("Clear master key must starts with hex: or base64:");
};
body_map.insert("clear_master_key_hex".to_string(), hex::encode(&clear_master_key).into());
}
if let Some(yubikey_challenge) = yubikey_challenge {
body_map.insert("yubikey_challenge".to_string(), yubikey_challenge.into());
@@ -87,17 +106,15 @@ async fn do_direct_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatc
}
async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let ssh_remote = sub_arg_matches.value_of("ssh-remote").map(|s| s.to_string());
let connect = sub_arg_matches.value_of("connect").expect("Get argument listen error");
let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
let client = Client::new();
let uri = format!("http://{}/status", connect);
debugging!("Request uri: {}", &uri);
let req = Request::builder().method(Method::GET).uri(uri).body(Body::empty())?;
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
return simple_error!("Server status is not success: {}", req_response.status().as_u16());
}
let data = response_to_value(req_response).await?;
let data = send_kms_request_with_ssh_enabled(&ssh_remote, true, &uri, &None).await?;
debugging!("Get status: {}", &data);
let status = &data["status"];
if let Some(status) = status.as_str() {
@@ -112,25 +129,98 @@ async fn do_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>
let instance_public_key_jwk = &data["instance_public_key_jwk"];
println!("Instance server public key JWK: {}", instance_public_key_jwk);
let line = read_line("Input encrypted master key: ")?;
let line = {
let line = read_line("Input clear(starts with hex: or base64:) or encrypted master key: ", read_from_pinentry)?;
let line = iff!(line.starts_with("hmac_enc:"), card_hmac_decrypt(&line)?, line);
if line.starts_with("hex:") || line.starts_with("base64:") {
let jwk = opt_result!(serde_json::to_string(&instance_public_key_jwk), "Serialize instance server public key JWK: {} failed");
master_key_encrypt(&line, &jwk)?
} else {
line
}
};
let uri = format!("http://{}/init", connect);
debugging!("Request uri: {}", &uri);
let body = json!({
"encrypted_master_key": line,
});
let body = serde_json::to_string(&body)?;
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data);
}
let _ = send_kms_request_with_ssh_enabled(&ssh_remote, false, &uri, &Some(body)).await?;
success!("Init finished");
Ok(Some(0))
}
fn card_hmac_decrypt(ciphertext: &str) -> XResult<String> {
let mut c = std::process::Command::new("card-cli");
c.args(&["hmac-decrypt", "--ciphertext", &ciphertext, "--json"]);
debugging!("Run command: {:?}", c);
let output = opt_result!(c.output(), "Call: {:?} failed: {}", c);
if !output.status.success() {
return simple_error!("Call: {:?} exit with error", output);
}
let data: Value = serde_json::from_slice(&output.stdout)?;
if let Value::Object(data_map) = &data {
if let Some(Value::String(plaintext)) = data_map.get("plaintext") {
return Ok(plaintext.to_string());
}
}
simple_error!("Hmac decrypt without plaintext, data: {:?}", data)
}
async fn send_kms_request_with_ssh_enabled(ssh_remote: &Option<String>, get_request: bool, uri: &str, body: &Option<String>) -> XResult<Value> {
match ssh_remote {
None => {
let client = Client::new();
let method = iff!(get_request, Method::GET, Method::POST);
let request_body = match body {
None => Body::empty(),
Some(body) => Body::from(body.clone()),
};
let req = Request::builder().method(method).uri(uri).body(request_body)?;
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
return simple_error!("Server status is not success: {}", req_response.status().as_u16());
}
let data = response_to_value(req_response).await?;
Ok(data)
}
Some(ssh_remote) => {
let mut c;
if ssh_remote == "localhost" {
c = std::process::Command::new("curl");
} else {
c = std::process::Command::new("ssh");
c.args([ssh_remote, "curl"]);
}
c.arg("-s");
if !get_request {
c.args(["-X", "POST"]);
}
if let Some(body) = body {
c.args(["-H", "x-body-based64-encoded:1"]);
c.args(["--data-raw", &STANDARD.encode(body).to_string()]);
}
c.arg(uri);
debugging!("Run command: {:?}", c);
let output = opt_result!(c.output(), "Call: {:?} failed: {}", c);
if !output.status.success() {
return simple_error!("Call: {:?} exit with error", output);
}
debugging!("Output: {:?}", output);
let data: Value = serde_json::from_slice(&output.stdout)?;
if let Value::Object(data_map) = &data {
if let Some(Value::String(error)) = data_map.get("error") {
return simple_error!("Get error: {}, details: {}", error, data);
}
}
Ok(data)
}
}
}
async fn do_read(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let body = if let Some(name) = sub_arg_matches.value_of("name") {
json!({ "name": name })
@@ -164,7 +254,7 @@ async fn do_write(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_
return simple_error!("Require one of value, value-hex, value-base64");
};
let data = do_inner_request(sub_arg_matches, "write", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?);
println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0))
}
@@ -183,7 +273,7 @@ async fn do_encrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
return simple_error!("Require one of value, value-hex, value-base64");
};
let data = do_inner_request(sub_arg_matches, "encrypt", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?);
println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0))
}
@@ -193,30 +283,59 @@ async fn do_decrypt(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<
let body = json!({ "encrypted_value": value });
let data = do_inner_request(sub_arg_matches, "decrypt", &body).await?;
success!("Value: {}", serde_json::to_string_pretty(&data)?);
println!("{}", serde_json::to_string_pretty(&data)?);
Ok(Some(0))
}
fn do_offline_init(_arg_matches: &ArgMatches<'_>, _sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let line = read_line("Input master key: ")?;
let master_key = if line.starts_with("hex:") {
let hex: String = line.chars().skip(4).collect();
hex::decode(&hex)?
} else if line.starts_with("base64:") {
let base64: String = line.chars().skip(7).collect();
STANDARD.decode(&base64)?
} else {
line.as_bytes().to_vec()
};
let jwk = read_line("Input JWK: ")?;
let rsa_public_key = jwk_to_rsa_pubic_key(&jwk)?;
let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &rsa_public_key)?;
fn do_offline_init(_arg_matches: &ArgMatches<'_>, sub_arg_matches: &ArgMatches<'_>) -> CommandError {
let read_from_pinentry = sub_arg_matches.is_present("read-from-pinentry");
let line = read_line("Input master key: ", read_from_pinentry)?;
let jwk = read_line("Input JWK: ", read_from_pinentry)?;
let encrypted_master_key = master_key_encrypt(&line, &jwk)?;
success!("Encrypted master key: {}", encrypted_master_key);
Ok(Some(0))
}
fn read_line(prompt: &str) -> XResult<String> {
fn master_key_encrypt(master_key: &str, jwk: &str) -> XResult<String> {
let master_key = if master_key.starts_with("hex:") {
let hex: String = master_key.chars().skip(4).collect();
hex::decode(&hex)?
} else if master_key.starts_with("base64:") {
let base64: String = master_key.chars().skip(7).collect();
STANDARD.decode(&base64)?
} else if master_key.starts_with("LKMS:") {
#[cfg(feature = "yubikey")]
{
use crate::yubikey_hmac;
// Yubikey Hmac encrypted key
let challenge = opt_result!(
pinentry_util::read_pin(Some("Input yubikey challenge"), Some("Challenge: ")), "Read challenge failed: {}");
let derived_key = yubikey_hmac::yubikey_challenge_as_32_bytes(challenge.get_pin().as_bytes())?;
let (key, _) = jose::deserialize_jwe_aes(master_key, &derived_key)?;
key
}
#[cfg(not(feature = "yubikey"))]
return simple_error!("Yubikey feature is not enabled.");
} else {
master_key.as_bytes().to_vec()
};
let rsa_public_key = jwk_to_rsa_pubic_key(jwk)?;
let encrypted_master_key = jose::serialize_jwe_rsa(&master_key, &rsa_public_key)?;
Ok(encrypted_master_key)
}
fn read_line(prompt: &str, pinentry: bool) -> XResult<String> {
if pinentry {
read_line_from_pinentry_util(prompt)
} else {
read_line_from_terminal(prompt)
}
}
fn read_line_from_terminal(prompt: &str) -> XResult<String> {
std::io::stdout().write_all(prompt.as_bytes()).ok();
std::io::stdout().flush().ok();
let mut line = String::new();
@@ -226,6 +345,11 @@ fn read_line(prompt: &str) -> XResult<String> {
Ok(line.trim().to_string())
}
fn read_line_from_pinentry_util(prompt: &str) -> XResult<String> {
let pin = opt_result!(pinentry_util::read_pin(Some(prompt.to_string()), Some("PIN:".to_string())), "Read from pin-entry failed: {}");
Ok(pin.get_pin().to_string())
}
async fn response_to_value(response: Response<Body>) -> XResult<Value> {
let req_body = response.into_body();
let whole_body = hyper::body::aggregate(req_body).await?;
@@ -241,10 +365,10 @@ async fn do_inner_request(sub_arg_matches: &ArgMatches<'_>, action: &str, body:
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data);
}
// if req_response.status() != StatusCode::OK {
// let status = req_response.status().as_u16();
// let data = response_to_value(req_response).await?;
// return simple_error!("Server status is not success: {}, response: {}", status, data);
// }
response_to_value(req_response).await
}

150
src/db.rs
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,41 @@
use std::io::{Cursor, Read};
use std::sync::Mutex;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use hyper::StatusCode;
use hyper::body::Buf;
use hyper::{Body, Request, StatusCode};
use rsa::RsaPrivateKey;
use rusqlite::Connection;
use rust_util::{opt_result, simple_error, XResult};
use seckey::SecBytes;
use serde::{Deserialize, Serialize};
use serde::{de, Deserialize, Serialize};
use serde_json::{json, Map, Value};
use zeroize::Zeroize;
use crate::db;
pub type GenericError = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = std::result::Result<T, GenericError>;
pub async fn parse_request<T>(req: Request<Body>) -> XResult<T>
where
T: de::DeserializeOwned,
{
let based64_encoded = req.headers().get("x-body-based64-encoded").is_some();
let whole_body = hyper::body::aggregate(req).await?;
let mut body = Vec::<u8>::new();
whole_body.reader().read_to_end(&mut body)?;
if based64_encoded {
let mut based64_decoded = opt_result!(STANDARD.decode(&body), "Decode request body base64 failed: {}");
body.clear();
body.extend_from_slice(&based64_decoded);
based64_decoded.zeroize();
}
let req_object = serde_json::from_reader(Cursor::new(&body))?;
body.zeroize();
Ok(req_object)
}
#[macro_export]
macro_rules! do_response {
($ex: expr) => (
@@ -34,11 +55,12 @@ pub fn ok(body: Value) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::OK, body))
}
pub fn error(error: &str) -> XResult<(StatusCode, Value)> {
Ok((
StatusCode::BAD_REQUEST,
json!({ "error": error })
))
pub fn client_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::BAD_REQUEST, json!({ "error": error })))
}
pub fn server_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": error })))
}
// pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> {
@@ -97,8 +119,20 @@ impl MultipleViewValue {
}
}
#[macro_export]
macro_rules! require_master_key {
() => {
match $crate::serve_common::get_master_key() {
None => return $crate::serve_common::client_error("status_not_ready"),
Some(key) => key,
}
};
}
pub fn get_master_key() -> Option<SecBytes> {
let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
let startup_rw_lock = STATUP_RW_LOCK
.lock()
.expect("Lock read startup rw lock error");
match &*startup_rw_lock {
None => None,
Some(k) => match &k.master_key {
@@ -131,11 +165,14 @@ pub fn byte_to_multi_view_map(bytes: &[u8], with_value: bool) -> Map<String, Val
}
pub fn open_local_db() -> XResult<Connection> {
let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
let startup_rw_lock = STATUP_RW_LOCK
.lock()
.expect("Lock read startup rw lock error");
match &*startup_rw_lock {
None => simple_error!("Db is not initted!"),
Some(k) => {
Ok(opt_result!(db::open_db(& k.database_file), "Open db failed: {}"))
}
None => simple_error!("Db is not initiated!"),
Some(k) => Ok(opt_result!(
db::open_db(&k.database_file),
"Open db failed: {}"
)),
}
}

View File

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

View File

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

View File

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

66
src/serve_log.rs Normal file
View File

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

View File

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

View File

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