Sealing: Encryption of Secrets #1
3 changed files with 56 additions and 16 deletions
|
|
@ -21,10 +21,12 @@ CREATE TABLE kv2_secret_version (
|
|||
secret_path TEXT NOT NULL,
|
||||
|
||||
version_number INTEGER NOT NULL CHECK ( version_number > 0 ),
|
||||
secret_data TEXT NOT NULL,
|
||||
created_time DATETIME NOT NULL,
|
||||
deletion_time DATETIME,
|
||||
|
||||
encrypted_data BLOB NOT NULL,
|
||||
nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ),
|
||||
|
||||
PRIMARY KEY (engine_path, secret_path, version_number),
|
||||
FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::structs::KvV2WriteRequest;
|
|||
use crate::{
|
||||
common::HttpError, engines::{
|
||||
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath
|
||||
}, DbPool
|
||||
}, storage::sealing::{seal, unseal}, DbPool
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
|
|
@ -10,9 +10,9 @@ use axum::{
|
|||
http::StatusCode,
|
||||
response::{IntoResponse, NoContent, Response},
|
||||
};
|
||||
use log::{error, info, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::Deserialize;
|
||||
use time::UtcDateTime;
|
||||
use time::{OffsetDateTime, UtcDateTime};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GetDataQuery {
|
||||
|
|
@ -22,25 +22,55 @@ pub struct GetDataQuery {
|
|||
pub version: u32,
|
||||
}
|
||||
|
||||
/// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations
|
||||
struct SecretDataInternal {
|
||||
pub created_time: OffsetDateTime,
|
||||
pub deletion_time: Option<OffsetDateTime>,
|
||||
pub version_number: i64,
|
||||
pub secret_path: String,
|
||||
|
||||
pub nonce: Vec<u8>,
|
||||
pub encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SecretDataInternal {
|
||||
pub async fn into_external(self) -> KvSecretData {
|
||||
let secret = unseal(self.nonce, &self.encrypted_data).await;
|
||||
KvSecretData {
|
||||
created_time: self.created_time,
|
||||
deletion_time: self.deletion_time,
|
||||
version_number: self.version_number,
|
||||
secret_path: self.secret_path,
|
||||
secret_data: secret,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_data(
|
||||
State(pool): State<DbPool>,
|
||||
Query(params): Query<GetDataQuery>,
|
||||
Path(path): Path<String>,
|
||||
Extension(EnginePath(engine_path)): Extension<EnginePath>,
|
||||
) -> Result<Response, ()> {
|
||||
let res = if params.version == 0 {
|
||||
debug!(
|
||||
"Get request: Engine: {}, path: {}",
|
||||
engine_path,
|
||||
path,
|
||||
);
|
||||
|
||||
let res = if params.version != 0 {
|
||||
// With specific version
|
||||
sqlx::query_as!(
|
||||
KvSecretData,
|
||||
r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path
|
||||
SecretDataInternal,
|
||||
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path
|
||||
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
|
||||
AND version_number = $3"#,
|
||||
engine_path, path, params.version).fetch_one(&pool).await
|
||||
} else {
|
||||
// Without specific version
|
||||
sqlx::query_as!(
|
||||
KvSecretData,
|
||||
r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path
|
||||
SecretDataInternal,
|
||||
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path
|
||||
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
|
||||
ORDER BY version_number DESC LIMIT 1"#,
|
||||
engine_path, path).fetch_one(&pool).await
|
||||
|
|
@ -48,8 +78,10 @@ pub async fn get_data(
|
|||
|
||||
match res {
|
||||
Ok(secret_content) => {
|
||||
let secret_content = secret_content.into_external().await;
|
||||
let inner = secret_content.secret_data;
|
||||
let data = Wrapper {
|
||||
data: serde_json::from_str(&secret_content.secret_data).unwrap(),
|
||||
data: serde_json::from_str(&inner).unwrap(),
|
||||
};
|
||||
let return_secret = KvSecretRes {
|
||||
data,
|
||||
|
|
@ -69,7 +101,7 @@ pub async fn get_data(
|
|||
"Secret not found within kv2 engine",
|
||||
))
|
||||
}
|
||||
_ => panic!("{e:?}"),
|
||||
_ => panic!("Unhandled error: {e:?}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +123,11 @@ pub async fn post_data(
|
|||
let created_time = time::UtcDateTime::now();
|
||||
let ts = created_time.unix_timestamp();
|
||||
|
||||
let content = serde_json::to_string(&secret.data).unwrap();
|
||||
|
||||
let (nonce, enc) = seal(&content).await;
|
||||
let nonce = nonce.as_slice();
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
let res_m = sqlx::query!("
|
||||
|
|
@ -103,7 +140,7 @@ pub async fn post_data(
|
|||
"src/engines/kv/post_secret.sql",
|
||||
engine_path,
|
||||
kv_path,
|
||||
secret.data,
|
||||
nonce, enc,
|
||||
ts,
|
||||
secret.version,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ WITH latest_version AS (
|
|||
FROM kv2_secret_version
|
||||
WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path
|
||||
)
|
||||
INSERT INTO kv2_secret_version (engine_path, secret_path, secret_data, created_time, version_number)
|
||||
INSERT INTO kv2_secret_version (engine_path, secret_path, nonce, encrypted_data, created_time, version_number)
|
||||
VALUES (
|
||||
$1, -- engine_path
|
||||
$2, -- secret_path
|
||||
$3, -- secret_data
|
||||
$4, -- created_time
|
||||
$3, -- nonce
|
||||
$4, -- encrypted_data
|
||||
$5, -- created_time
|
||||
CASE -- Use provided version if given
|
||||
WHEN $5 IS NOT NULL THEN $5 -- version_number (optional)
|
||||
WHEN $6 IS NOT NULL THEN $6 -- version_number (optional)
|
||||
ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 1) -- otherwise 1
|
||||
END -- version_number logic
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue