Sealing: Encryption of Secrets #1

Merged
C0ffeeCode merged 7 commits from sealing into dev 2025-04-03 10:08:08 +02:00
3 changed files with 56 additions and 16 deletions
Showing only changes of commit 4d342e8b99 - Show all commits

View file

@ -21,10 +21,12 @@ CREATE TABLE kv2_secret_version (
secret_path TEXT NOT NULL, secret_path TEXT NOT NULL,
version_number INTEGER NOT NULL CHECK ( version_number > 0 ), version_number INTEGER NOT NULL CHECK ( version_number > 0 ),
secret_data TEXT NOT NULL,
created_time DATETIME NOT NULL, created_time DATETIME NOT NULL,
deletion_time DATETIME, deletion_time DATETIME,
encrypted_data BLOB NOT NULL,
nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ),
PRIMARY KEY (engine_path, secret_path, version_number), PRIMARY KEY (engine_path, secret_path, version_number),
FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path) FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path)
); );

View file

@ -2,7 +2,7 @@ use super::structs::KvV2WriteRequest;
use crate::{ use crate::{
common::HttpError, engines::{ common::HttpError, engines::{
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath
}, DbPool }, storage::sealing::{seal, unseal}, DbPool
}; };
use axum::{ use axum::{
Extension, Json, Extension, Json,
@ -10,9 +10,9 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, NoContent, Response}, response::{IntoResponse, NoContent, Response},
}; };
use log::{error, info, warn}; use log::{debug, error, info, warn};
use serde::Deserialize; use serde::Deserialize;
use time::UtcDateTime; use time::{OffsetDateTime, UtcDateTime};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct GetDataQuery { pub struct GetDataQuery {
@ -22,25 +22,55 @@ pub struct GetDataQuery {
pub version: u32, 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( pub async fn get_data(
State(pool): State<DbPool>, State(pool): State<DbPool>,
Query(params): Query<GetDataQuery>, Query(params): Query<GetDataQuery>,
Path(path): Path<String>, Path(path): Path<String>,
Extension(EnginePath(engine_path)): Extension<EnginePath>, Extension(EnginePath(engine_path)): Extension<EnginePath>,
) -> Result<Response, ()> { ) -> 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 // With specific version
sqlx::query_as!( sqlx::query_as!(
KvSecretData, SecretDataInternal,
r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path 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 FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
AND version_number = $3"#, AND version_number = $3"#,
engine_path, path, params.version).fetch_one(&pool).await engine_path, path, params.version).fetch_one(&pool).await
} else { } else {
// Without specific version // Without specific version
sqlx::query_as!( sqlx::query_as!(
KvSecretData, SecretDataInternal,
r#"SELECT secret_data, created_time, deletion_time, version_number, secret_path 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 FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
ORDER BY version_number DESC LIMIT 1"#, ORDER BY version_number DESC LIMIT 1"#,
engine_path, path).fetch_one(&pool).await engine_path, path).fetch_one(&pool).await
@ -48,8 +78,10 @@ pub async fn get_data(
match res { match res {
Ok(secret_content) => { Ok(secret_content) => {
let secret_content = secret_content.into_external().await;
let inner = secret_content.secret_data;
let data = Wrapper { let data = Wrapper {
data: serde_json::from_str(&secret_content.secret_data).unwrap(), data: serde_json::from_str(&inner).unwrap(),
}; };
let return_secret = KvSecretRes { let return_secret = KvSecretRes {
data, data,
@ -69,7 +101,7 @@ pub async fn get_data(
"Secret not found within kv2 engine", "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 created_time = time::UtcDateTime::now();
let ts = created_time.unix_timestamp(); 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 mut tx = pool.begin().await.unwrap();
let res_m = sqlx::query!(" let res_m = sqlx::query!("
@ -103,7 +140,7 @@ pub async fn post_data(
"src/engines/kv/post_secret.sql", "src/engines/kv/post_secret.sql",
engine_path, engine_path,
kv_path, kv_path,
secret.data, nonce, enc,
ts, ts,
secret.version, secret.version,
) )

View file

@ -4,14 +4,15 @@ WITH latest_version AS (
FROM kv2_secret_version FROM kv2_secret_version
WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path 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 ( VALUES (
$1, -- engine_path $1, -- engine_path
$2, -- secret_path $2, -- secret_path
$3, -- secret_data $3, -- nonce
$4, -- created_time $4, -- encrypted_data
$5, -- created_time
CASE -- Use provided version if given 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 ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 1) -- otherwise 1
END -- version_number logic END -- version_number logic
) )