From 4d342e8b99a6de6187e834e5a7854fadc55677a8 Mon Sep 17 00:00:00 2001 From: C0ffeeCode Date: Wed, 26 Mar 2025 21:51:27 +0100 Subject: [PATCH] Feat (kv2): Support Sealing --- migrations/20240501152243_KvSecret.sql | 4 +- src/engines/kv/data.rs | 59 +++++++++++++++++++++----- src/engines/kv/post_secret.sql | 9 ++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/migrations/20240501152243_KvSecret.sql b/migrations/20240501152243_KvSecret.sql index 6736fd0..60e8bc6 100644 --- a/migrations/20240501152243_KvSecret.sql +++ b/migrations/20240501152243_KvSecret.sql @@ -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) ); diff --git a/src/engines/kv/data.rs b/src/engines/kv/data.rs index 31c2449..1854cd2 100644 --- a/src/engines/kv/data.rs +++ b/src/engines/kv/data.rs @@ -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, + pub version_number: i64, + pub secret_path: String, + + pub nonce: Vec, + pub encrypted_data: Vec, +} + +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, Query(params): Query, Path(path): Path, Extension(EnginePath(engine_path)): Extension, ) -> Result { - 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, ) diff --git a/src/engines/kv/post_secret.sql b/src/engines/kv/post_secret.sql index 920d7b7..9f44d07 100644 --- a/src/engines/kv/post_secret.sql +++ b/src/engines/kv/post_secret.sql @@ -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 )