This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+9 -108
View File
@@ -59,10 +59,7 @@ fn read_key(path: &Path) -> Result<Zeroizing<[u8; KEY_BYTES]>> {
metadata.is_file() && !metadata.file_type().is_symlink(),
"secret store master key must be a regular file"
);
ensure!(
metadata.len() == KEY_BYTES as u64,
"invalid secret store master key size"
);
ensure!(metadata.len() == KEY_BYTES as u64, "invalid secret store master key size");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
@@ -82,10 +79,7 @@ fn read_key(path: &Path) -> Result<Zeroizing<[u8; KEY_BYTES]>> {
let mut file = options.open(path)?;
file.read_exact(bytes.as_mut())?;
let mut extra = [0u8; 1];
ensure!(
file.read(&mut extra)? == 0,
"invalid secret store master key size"
);
ensure!(file.read(&mut extra)? == 0, "invalid secret store master key size");
Ok(bytes)
}
@@ -164,10 +158,7 @@ pub async fn put(
metadata: serde_json::Value,
) -> Result<()> {
validate_identity(secret_id, provider)?;
ensure!(
!plaintext.is_empty() && plaintext.len() <= MAX_SECRET_BYTES,
"invalid secret size"
);
ensure!(!plaintext.is_empty() && plaintext.len() <= MAX_SECRET_BYTES, "invalid secret size");
let metadata = serde_json::to_string(&metadata)?;
ensure!(metadata.len() <= 4096, "secret metadata is too large");
initialize(data_dir)?;
@@ -221,16 +212,12 @@ pub async fn get(
}
validate_identity(secret_id, &provider)?;
let version: i64 = row.try_get("encryption_version")?;
ensure!(
version == ENCRYPTION_VERSION,
"unsupported secret encryption version"
);
ensure!(version == ENCRYPTION_VERSION, "unsupported secret encryption version");
let nonce: Vec<u8> = row.try_get("nonce")?;
ensure!(nonce.len() == NONCE_BYTES, "invalid secret nonce");
let mut ciphertext = Zeroizing::new(row.try_get::<Vec<u8>, _>("ciphertext")?);
ensure!(
ciphertext.len() >= AES_256_GCM.tag_len()
&& ciphertext.len() <= MAX_SECRET_BYTES + AES_256_GCM.tag_len(),
ciphertext.len() >= AES_256_GCM.tag_len() && ciphertext.len() <= MAX_SECRET_BYTES + AES_256_GCM.tag_len(),
"invalid encrypted secret size"
);
let mut nonce_bytes = [0u8; NONCE_BYTES];
@@ -300,10 +287,7 @@ pub async fn migrate_acme_credentials(
return Ok(0);
}
let root_meta = fs::symlink_metadata(&root)?;
ensure!(
root_meta.is_dir() && !root_meta.file_type().is_symlink(),
"invalid ACME directory"
);
ensure!(root_meta.is_dir() && !root_meta.file_type().is_symlink(), "invalid ACME directory");
let mut migrated = 0usize;
for entry in fs::read_dir(&root)? {
let entry = entry?;
@@ -317,10 +301,7 @@ pub async fn migrate_acme_credentials(
continue;
}
let profile: serde_json::Value = serde_json::from_slice(&fs::read(&profile_path)?)?;
let provider = profile
.get("provider")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let provider = profile.get("provider").and_then(serde_json::Value::as_str).unwrap_or("");
let certificate_id = profile
.get("certificate_id")
.and_then(serde_json::Value::as_str)
@@ -360,8 +341,7 @@ pub async fn migrate_acme_credentials(
)
.await?;
plaintext.zeroize();
fs::remove_file(&credential_path)
.context("legacy ACME credential was encrypted but could not be removed")?;
fs::remove_file(&credential_path).context("legacy ACME credential was encrypted but could not be removed")?;
migrated += 1;
}
Ok(migrated)
@@ -392,83 +372,4 @@ pub fn write_restored_master_key(data_dir: &Path, key: &[u8]) -> Result<()> {
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn secret_round_trip_is_encrypted_in_sqlite_and_survives_key_reload() {
let dir = tempfile::tempdir().unwrap();
crate::auth::prepare_directory(dir.path()).unwrap();
let database = dir.path().join("config.sqlite3");
crate::auth::prepare_database(&database).unwrap();
let store = crate::db::Store::open(&database, &crate::config::Config::default())
.await
.unwrap();
initialize(dir.path()).unwrap();
let secret = b"cloudflare-example-token-0123456789";
put(
&store,
dir.path(),
"provider/cloudflare/demo/api_token",
"cloudflare",
secret,
serde_json::json!({"id":"demo"}),
)
.await
.unwrap();
let row: Vec<u8> = sqlx::query_scalar(
"SELECT ciphertext FROM secret_store WHERE secret_id='provider/cloudflare/demo/api_token'",
)
.fetch_one(&store.pool)
.await
.unwrap();
assert!(!row.windows(secret.len()).any(|window| window == secret));
let value = get(
&store,
dir.path(),
"provider/cloudflare/demo/api_token",
Some("cloudflare"),
)
.await
.unwrap();
assert_eq!(value.as_slice(), secret);
drop(value);
assert_eq!(
read_key(&master_key_path(dir.path())).unwrap().len(),
KEY_BYTES
);
}
#[tokio::test]
async fn tampered_ciphertext_is_rejected() {
let dir = tempfile::tempdir().unwrap();
crate::auth::prepare_directory(dir.path()).unwrap();
let database = dir.path().join("config.sqlite3");
crate::auth::prepare_database(&database).unwrap();
let store = crate::db::Store::open(&database, &crate::config::Config::default())
.await
.unwrap();
put(
&store,
dir.path(),
"acme/site/cloudflare/credential",
"cloudflare",
b"secret-value",
serde_json::json!({}),
)
.await
.unwrap();
sqlx::query("UPDATE secret_store SET ciphertext = zeroblob(length(ciphertext))")
.execute(&store.pool)
.await
.unwrap();
assert!(get(
&store,
dir.path(),
"acme/site/cloudflare/credential",
Some("cloudflare")
)
.await
.is_err());
}
}
include!("secret_store/tests.rs");