feat: v1.0.4, generate data key and save to db

This commit is contained in:
2024-11-22 23:42:04 +08:00
parent 20ad9e6bd7
commit 87cba2be57
10 changed files with 118 additions and 64 deletions

10
Cargo.lock generated
View File

@@ -365,9 +365,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -997,7 +997,7 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "local-mini-kms" name = "local-mini-kms"
version = "1.0.3" version = "1.0.4"
dependencies = [ dependencies = [
"aes-gcm-stream", "aes-gcm-stream",
"aes-kw", "aes-kw",
@@ -1334,9 +1334,9 @@ checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.91" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "307e3004becf10f5a6e0d59d20f3cd28231b0e0827a96cd3e0ce6d14bc1e4bb3" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "local-mini-kms" name = "local-mini-kms"
version = "1.0.3" version = "1.0.4"
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

View File

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

View File

@@ -241,10 +241,10 @@ async fn do_inner_request(sub_arg_matches: &ArgMatches<'_>, action: &str, body:
let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?; let req = Request::builder().method(Method::POST).uri(uri).body(Body::from(body))?;
let req_response = client.request(req).await?; let req_response = client.request(req).await?;
if req_response.status() != StatusCode::OK { // if req_response.status() != StatusCode::OK {
let status = req_response.status().as_u16(); // let status = req_response.status().as_u16();
let data = response_to_value(req_response).await?; // let data = response_to_value(req_response).await?;
return simple_error!("Server status is not success: {}, response: {}", status, data); // return simple_error!("Server status is not success: {}, response: {}", status, data);
} // }
response_to_value(req_response).await response_to_value(req_response).await
} }

View File

@@ -9,10 +9,14 @@ pub struct Key {
pub comment: Option<String>, pub comment: Option<String>,
} }
pub fn make_db_key_name(name: &str) -> String { pub fn make_value_key_name(name: &str) -> String {
format!("value:{}", name) format!("value:{}", name)
} }
pub fn make_data_key_name(name: &str) -> String {
format!("data_key:{}", name)
}
pub fn open_db(db: &str) -> XResult<Connection> { pub fn open_db(db: &str) -> XResult<Connection> {
let con = opt_result!(Connection::open(db), "Open sqlite db: {}, failed: {}", db); let con = opt_result!(Connection::open(db), "Open sqlite db: {}, failed: {}", db);
log::debug!("Db auto commit: {}", con.is_autocommit()); log::debug!("Db auto commit: {}", con.is_autocommit());
@@ -42,6 +46,7 @@ pub fn init_db(conn: &Connection) -> XResult<bool> {
pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> { pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> {
let default_comment = "".to_string(); let default_comment = "".to_string();
log::debug!("insert key name={}", &key.name);
let _ = conn.execute( let _ = conn.execute(
"INSERT INTO keys (name, value, comment) VALUES (?1, ?2, ?3)", "INSERT INTO keys (name, value, comment) VALUES (?1, ?2, ?3)",
(&key.name, &key.encrypted_key, key.comment.as_ref().unwrap_or(&default_comment)), (&key.name, &key.encrypted_key, key.comment.as_ref().unwrap_or(&default_comment)),
@@ -50,6 +55,7 @@ pub fn insert_key(conn: &Connection, key: &Key) -> XResult<()> {
} }
pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> { pub fn update_key(conn: &Connection, key: &Key) -> XResult<()> {
log::debug!("update key name={}", &key.name);
if let Some(comment) = &key.comment { if let Some(comment) = &key.comment {
let _ = conn.execute( let _ = conn.execute(
"UPDATE keys SET value = ?1, comment = ?2 WHERE name = ?3", "UPDATE keys SET value = ?1, comment = ?2 WHERE name = ?3",
@@ -74,8 +80,14 @@ pub fn find_key(conn: &Connection, name: &str) -> XResult<Option<Key>> {
}) })
})?; })?;
match key_iter.next() { match key_iter.next() {
None => Ok(None), None => {
Some(Ok(r)) => Ok(Some(r)), log::debug!("key name={} not exists", name);
Ok(None)
}
Some(Ok(r)) => {
log::debug!("found key name={}", name);
Ok(Some(r))
}
Some(Err(e)) => simple_error!("Find key failed: {}", e), Some(Err(e)) => simple_error!("Find key failed: {}", e),
} }
} }

View File

@@ -34,13 +34,20 @@ pub fn ok(body: Value) -> XResult<(StatusCode, Value)> {
Ok((StatusCode::OK, body)) Ok((StatusCode::OK, body))
} }
pub fn error(error: &str) -> XResult<(StatusCode, Value)> { pub fn client_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok(( Ok((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
json!({ "error": error }) json!({ "error": error })
)) ))
} }
pub fn server_error(error: &str) -> XResult<(StatusCode, Value)> {
Ok((
StatusCode::INTERNAL_SERVER_ERROR,
json!({ "error": error })
))
}
// pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> { // pub fn bad_request(error: &str, error_message: &str) -> XResult<(StatusCode, Value)> {
// Ok(( // Ok((
// StatusCode::BAD_REQUEST, // StatusCode::BAD_REQUEST,
@@ -97,6 +104,15 @@ impl MultipleViewValue {
} }
} }
#[macro_export] macro_rules! require_master_key {
() => {
match $crate::serve_common::get_master_key() {
None => return $crate::serve_common::client_error("status_not_ready"),
Some(key) => key,
}
};
}
pub fn get_master_key() -> Option<SecBytes> { pub fn get_master_key() -> Option<SecBytes> {
let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error"); let startup_rw_lock = STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
match &*startup_rw_lock { match &*startup_rw_lock {

View File

@@ -1,5 +1,6 @@
use crate::serve_common::{get_master_key, Result}; use crate::db::Key;
use crate::{do_response, jose, serve_common}; use crate::serve_common::{open_local_db, Result};
use crate::{db, do_response, jose, require_master_key, serve_common};
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
use hyper::body::Buf; use hyper::body::Buf;
@@ -11,7 +12,6 @@ use serde_derive::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
// type:aes, spec:128,192,256 // type:aes, spec:128,192,256
// type:rsa, spec:2048,3072,4096 // type:rsa, spec:2048,3072,4096
// type:ec, spec:p256,p384,p521,ed25519,cv25519 // type:ec, spec:p256,p384,p521,ed25519,cv25519
@@ -20,6 +20,7 @@ struct DataKeyRequest {
r#type: String, r#type: String,
spec: String, spec: String,
name: Option<String>, name: Option<String>,
comment: Option<String>,
exportable: Option<bool>, exportable: Option<bool>,
return_plaintext: Option<bool>, return_plaintext: Option<bool>,
} }
@@ -33,24 +34,21 @@ async fn inner_generate(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let request: DataKeyRequest = serde_json::from_reader(whole_body.reader())?; let request: DataKeyRequest = serde_json::from_reader(whole_body.reader())?;
log::debug!("Generate data key: {} {}", &request.r#type, &request.spec); log::debug!("Generate data key: {} {}", &request.r#type, &request.spec);
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let exportable = request.exportable.unwrap_or(true); let exportable = request.exportable.unwrap_or(true);
let ret_key_plaintext = iff!(!exportable, false, request.return_plaintext.unwrap_or(false)); 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()) { 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", "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", "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), ("aes", "256") => generate_aes("datakey:aes-256", exportable, key, 32, ret_key_plaintext),
// TODO rsa 2048, rsa 3072, rsa 4096 // TODO rsa 2048, rsa 3072, rsa 4096
// TODO ec p256, p384, p521, ed25519, cv25519 // TODO ec p256, p384, p521, ed25519, cv25519
_ => return serve_common::error("invalid key_type or key_spec"), _ => return serve_common::client_error("invalid key_type or key_spec"),
}; };
match response_result { match response_result {
Err(e) => serve_common::error(&format!("internal error: {}", e)), Err(e) => serve_common::server_error(&format!("internal error: {}", e)),
Ok((key_plaintext, key_ciphertext)) => { Ok((key_plaintext, key_ciphertext)) => {
let mut map = Map::new(); let mut map = Map::new();
map.insert("key_type".to_string(), Value::String(request.r#type)); map.insert("key_type".to_string(), Value::String(request.r#type));
@@ -58,11 +56,24 @@ async fn inner_generate(req: Request<Body>) -> XResult<(StatusCode, Value)> {
if let Some(key_plaintext) = key_plaintext { if let Some(key_plaintext) = key_plaintext {
map.insert("key_plaintext".to_string(), Value::String(STANDARD.encode(&key_plaintext))); map.insert("key_plaintext".to_string(), Value::String(STANDARD.encode(&key_plaintext)));
} }
map.insert("key_ciphertext".to_string(), Value::String(key_ciphertext)); map.insert("key_ciphertext".to_string(), Value::String(key_ciphertext.clone()));
if let Some(_name) = &request.name { if let Some(name) = &request.name {
// TODO write datakey to db if name.is_empty() {
// TODO let data_key_name = format!("datakey:{}", name); return serve_common::client_error("name_is_empty");
}
let conn = open_local_db()?;
let db_key_name = db::make_data_key_name(name);
let db_key = db::find_key(&conn, &db_key_name)?;
if db_key.is_some() {
return serve_common::client_error("name_exists");
}
let key = Key {
name: db_key_name,
encrypted_key: key_ciphertext,
comment: request.comment,
};
db::insert_key(&conn, &key)?;
} }
Ok((StatusCode::OK, Value::Object(map))) Ok((StatusCode::OK, Value::Object(map)))

View File

@@ -4,9 +4,9 @@ use rust_util::XResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
use crate::do_response;
use crate::jose; use crate::jose;
use crate::serve_common::{self, byte_to_multi_view_map, get_master_key, MultipleViewValue, Result}; use crate::serve_common::{self, byte_to_multi_view_map, MultipleViewValue, Result};
use crate::{do_response, require_master_key};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct DecryptRequest { struct DecryptRequest {
@@ -22,16 +22,13 @@ async fn inner_decrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?; let data: DecryptRequest = serde_json::from_reader(whole_body.reader())?;
log::debug!("To be decrypted value: {}", &data.encrypted_value); log::debug!("To be decrypted value: {}", &data.encrypted_value);
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key.read()); let decrypted_value = jose::deserialize_jwe_aes(&data.encrypted_value, &key.read());
drop(key); drop(key);
let (data, header) = decrypted_value?; let (data, header) = decrypted_value?;
if let Some(false) = header.exportable { if let Some(false) = header.exportable {
return serve_common::error("data_not_exportable"); return serve_common::client_error("data_not_exportable");
} }
let mut map = byte_to_multi_view_map(&data, true); let mut map = byte_to_multi_view_map(&data, true);
@@ -58,10 +55,7 @@ async fn inner_encrypt(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let whole_body = hyper::body::aggregate(req).await?; let whole_body = hyper::body::aggregate(req).await?;
let data: MultipleViewValue = serde_json::from_reader(whole_body.reader())?; let data: MultipleViewValue = serde_json::from_reader(whole_body.reader())?;
let value = data.to_bytes()?; let value = data.to_bytes()?;
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let encrypt_result = jose::serialize_jwe_aes(&value, &key.read()); let encrypt_result = jose::serialize_jwe_aes(&value, &key.read());
drop(key); drop(key);

View File

@@ -37,9 +37,9 @@ async fn inner_init(req: Request<Body>) -> XResult<(StatusCode, Value)> {
pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCode, Value)> { pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCode, Value)> {
let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error"); let mut startup_rw_lock = serve_common::STATUP_RW_LOCK.lock().expect("Lock read startup rw lock error");
match &*startup_rw_lock { match &*startup_rw_lock {
None => return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))), None => return serve_common::server_error("instant_key_pair_not_initialized"),
Some(memory_key) => if memory_key.master_key.is_some() { Some(memory_key) => if memory_key.master_key.is_some() {
return Ok((StatusCode::BAD_REQUEST, json!({ "error": "bad_request", "error_message": "already init " }))); return serve_common::client_error("already_initialized");
}, },
} }
@@ -53,14 +53,14 @@ pub async fn inner_init_request(init_request: InitRequest) -> XResult<(StatusCod
let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair)?; let (clear_master_key, _) = jose::deserialize_jwe_rsa(&encrypted_master_key, &k.instance_rsa_key_pair)?;
clear_master_key clear_master_key
} else { } else {
return Ok((StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": "internal_error", "error_message": "not init " }))); return serve_common::server_error("instant_key_pair_not_initialized");
} }
} else { } else {
return serve_common::error("master_key_missing"); return serve_common::client_error("master_key_missing");
}; };
if clear_master_key.len() != 32 { if clear_master_key.len() != 32 {
return serve_common::error("bad_master_key_length"); return serve_common::client_error("bad_master_key_length");
} }
if let Some(k) = &mut *startup_rw_lock { if let Some(k) = &mut *startup_rw_lock {

View File

@@ -1,7 +1,7 @@
use crate::db::Key; use crate::db::Key;
use crate::do_response; use crate::serve_common::{self, byte_to_multi_view_map, open_local_db, MultipleViewValue, Result};
use crate::serve_common::{self, byte_to_multi_view_map, get_master_key, open_local_db, MultipleViewValue, Result};
use crate::{db, jose}; use crate::{db, jose};
use crate::{do_response, require_master_key};
use hyper::body::Buf; use hyper::body::Buf;
use hyper::{Body, Request, Response, StatusCode}; use hyper::{Body, Request, Response, StatusCode};
use rust_util::XResult; use rust_util::XResult;
@@ -30,22 +30,19 @@ async fn inner_read(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let named: Named = serde_json::from_reader(whole_body.reader())?; let named: Named = serde_json::from_reader(whole_body.reader())?;
let name = &named.name; let name = &named.name;
if name.is_empty() { if name.is_empty() {
return serve_common::error("key_name_is_empty"); return serve_common::client_error("name_is_empty");
} }
let db_key_name = db::make_db_key_name(name); let db_key_name = db::make_value_key_name(name);
let conn = open_local_db()?; let conn = open_local_db()?;
let db_key = db::find_key(&conn, &db_key_name)?; let db_key = db::find_key(&conn, &db_key_name)?;
let db_key_value = match db_key { let db_key_value = match db_key {
None => return serve_common::error("key_name_not_exists"), None => return serve_common::client_error("name_not_exists"),
Some(k) => k, Some(k) => k,
}; };
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?; let data = jose::deserialize_jwe_aes(&db_key_value.encrypted_key, &key.read())?;
drop(key); drop(key);
@@ -64,23 +61,20 @@ async fn inner_write(req: Request<Body>) -> XResult<(StatusCode, Value)> {
let named_value: NamedValue = serde_json::from_reader(whole_body.reader())?; let named_value: NamedValue = serde_json::from_reader(whole_body.reader())?;
let name = &named_value.name; let name = &named_value.name;
if name.is_empty() { if name.is_empty() {
return serve_common::error("key_name_is_empty"); return serve_common::client_error("name_is_empty");
} }
let db_key_name = db::make_db_key_name(name); let db_key_name = db::make_value_key_name(name);
let force_write = named_value.force_write.unwrap_or(false); let force_write = named_value.force_write.unwrap_or(false);
let conn = open_local_db()?; let conn = open_local_db()?;
let db_key = db::find_key(&conn, &db_key_name)?; let db_key = db::find_key(&conn, &db_key_name)?;
if db_key.is_some() && !force_write { if db_key.is_some() && !force_write {
return serve_common::error("key_name_exists"); return serve_common::client_error("name_exists");
} }
let value = named_value.value.to_bytes()?; let value = named_value.value.to_bytes()?;
let key = match get_master_key() { let key = require_master_key!();
None => return serve_common::error("status_not_ready"),
Some(key) => key,
};
let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?; let encrypt_value = jose::serialize_jwe_aes(&value, &key.read())?;
drop(key); drop(key);