feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
@@ -1,24 +1,63 @@
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use aether_crypto::{decrypt_python_fernet_ciphertext, encrypt_python_fernet_plaintext};
use serde_json::{Map, Value};
use uuid::Uuid;
use crate::{
GeminiVideoTaskSeed, LocalVideoTaskReadResponse, LocalVideoTaskRegistryMutation,
LocalVideoTaskSnapshot, OpenAiVideoTaskSeed, VideoTaskRegistry, VideoTaskStore,
};
#[derive(Debug, Default)]
#[derive(Default)]
pub struct InMemoryVideoTaskStore {
registry: Mutex<VideoTaskRegistry>,
}
#[derive(Debug)]
impl std::fmt::Debug for InMemoryVideoTaskStore {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("InMemoryVideoTaskStore")
.field("registry", &"[redacted]")
.finish()
}
}
pub struct FileVideoTaskStore {
path: PathBuf,
encryption_key: String,
registry: Mutex<VideoTaskRegistry>,
persisted_file: Mutex<PersistedVideoTaskStore>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum PersistedVideoTaskStore {
Missing,
Bytes(Vec<u8>),
}
struct LoadedVideoTaskRegistry {
registry: VideoTaskRegistry,
persisted_file: PersistedVideoTaskStore,
needs_rewrite: bool,
}
impl std::fmt::Debug for FileVideoTaskStore {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FileVideoTaskStore")
.field("path", &self.path)
.field("encryption_key", &"[redacted]")
.field("registry", &"[redacted]")
.finish()
}
}
const ENCRYPTED_VIDEO_TASK_STORE_PREFIX: &str = "aether-video-tasks-v2\n";
const LEGACY_ENCRYPTED_VIDEO_TASK_STORE_PREFIX: &str = "aether-video-tasks-v1\n";
const VIDEO_TASK_STORE_PURPOSE: &str = "video-task-file-store";
impl VideoTaskStore for InMemoryVideoTaskStore {
fn insert(&self, snapshot: LocalVideoTaskSnapshot) {
if let Ok(mut registry) = self.registry.lock() {
@@ -75,36 +114,107 @@ impl VideoTaskStore for InMemoryVideoTaskStore {
}
impl FileVideoTaskStore {
pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
pub fn new(
path: impl Into<PathBuf>,
encryption_key: impl Into<String>,
) -> std::io::Result<Self> {
let path = path.into();
let registry = Self::load_registry(&path)?;
Ok(Self {
let encryption_key = encryption_key.into();
if encryption_key.trim().is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"video task file store encryption key cannot be empty",
));
}
let LoadedVideoTaskRegistry {
registry,
persisted_file,
needs_rewrite,
} = Self::load_registry(&path, &encryption_key)?;
let store = Self {
path,
encryption_key,
registry: Mutex::new(registry),
})
persisted_file: Mutex::new(persisted_file),
};
if needs_rewrite {
let registry = store
.registry
.lock()
.map_err(|_| std::io::Error::other("video task store lock poisoned"))?;
store.persist_registry(&registry)?;
}
Ok(store)
}
fn load_registry(path: &Path) -> std::io::Result<VideoTaskRegistry> {
if !path.exists() {
return Ok(VideoTaskRegistry::default());
}
let bytes = std::fs::read(path)?;
fn load_registry(
path: &Path,
encryption_key: &str,
) -> std::io::Result<LoadedVideoTaskRegistry> {
let persisted_file = read_persisted_video_task_store(path)?;
let PersistedVideoTaskStore::Bytes(bytes) = &persisted_file else {
return Ok(LoadedVideoTaskRegistry {
registry: VideoTaskRegistry::default(),
persisted_file,
needs_rewrite: false,
});
};
if bytes.is_empty() {
return Ok(VideoTaskRegistry::default());
return Err(invalid_store_data("video task store is empty"));
}
serde_json::from_slice(&bytes)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
if let Some(ciphertext) = bytes.strip_prefix(ENCRYPTED_VIDEO_TASK_STORE_PREFIX.as_bytes()) {
let ciphertext = std::str::from_utf8(ciphertext)
.map_err(|_| invalid_store_data("encrypted video task store is not UTF-8"))?;
let protected = decrypt_python_fernet_ciphertext(encryption_key, ciphertext.trim())
.map_err(|_| invalid_store_data("video task store decryption failed"))?;
let plaintext = protected
.strip_prefix(VIDEO_TASK_STORE_PURPOSE)
.and_then(|value| value.strip_prefix('\0'))
.ok_or_else(|| invalid_store_data("video task store purpose mismatch"))?;
let mut registry: VideoTaskRegistry = serde_json::from_str(&plaintext)
.map_err(|_| invalid_store_data("decrypted video task store is invalid"))?;
let needs_rewrite = registry.sanitize_persisted_diagnostics();
return Ok(LoadedVideoTaskRegistry {
registry,
persisted_file,
needs_rewrite,
});
}
if let Some(ciphertext) =
bytes.strip_prefix(LEGACY_ENCRYPTED_VIDEO_TASK_STORE_PREFIX.as_bytes())
{
let ciphertext = std::str::from_utf8(ciphertext)
.map_err(|_| invalid_store_data("encrypted video task store is not UTF-8"))?;
let plaintext = decrypt_python_fernet_ciphertext(encryption_key, ciphertext.trim())
.map_err(|_| invalid_store_data("video task store decryption failed"))?;
let mut registry: VideoTaskRegistry = serde_json::from_str(&plaintext)
.map_err(|_| invalid_store_data("decrypted video task store is invalid"))?;
registry.sanitize_persisted_diagnostics();
return Ok(LoadedVideoTaskRegistry {
registry,
persisted_file,
needs_rewrite: true,
});
}
if bytes.starts_with(b"aether-") {
return Err(invalid_store_data(
"unsupported encrypted video task store envelope",
));
}
Err(invalid_store_data(
"plaintext video task stores are not accepted",
))
}
fn persist_registry(&self, registry: &VideoTaskRegistry) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let bytes = serde_json::to_vec_pretty(registry)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
let temp_path = self.path.with_extension("tmp");
std::fs::write(&temp_path, bytes)?;
std::fs::rename(temp_path, &self.path)?;
let bytes = encrypted_video_task_store_bytes(&self.encryption_key, registry)?;
let mut persisted_file = self
.persisted_file
.lock()
.map_err(|_| std::io::Error::other("video task persisted-file lock poisoned"))?;
replace_video_task_store_if_unchanged(&self.path, &persisted_file, &bytes)?;
*persisted_file = PersistedVideoTaskStore::Bytes(bytes);
Ok(())
}
@@ -112,13 +222,117 @@ impl FileVideoTaskStore {
let Ok(mut registry) = self.registry.lock() else {
return false;
};
if !mutator(&mut registry) {
let mut updated_registry = registry.clone();
if !mutator(&mut updated_registry) {
return false;
}
self.persist_registry(&registry).is_ok()
if self.persist_registry(&updated_registry).is_err() {
return false;
}
*registry = updated_registry;
true
}
}
fn invalid_store_data(message: &'static str) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, message)
}
fn encrypted_video_task_store_bytes(
encryption_key: &str,
registry: &VideoTaskRegistry,
) -> std::io::Result<Vec<u8>> {
let plaintext = serde_json::to_string(registry)
.map_err(|_| invalid_store_data("video task store serialization failed"))?;
let protected = format!("{VIDEO_TASK_STORE_PURPOSE}\0{plaintext}");
let ciphertext = encrypt_python_fernet_plaintext(encryption_key, &protected)
.map_err(|_| invalid_store_data("video task store encryption failed"))?;
let mut bytes = ENCRYPTED_VIDEO_TASK_STORE_PREFIX.as_bytes().to_vec();
bytes.extend_from_slice(ciphertext.as_bytes());
bytes.push(b'\n');
Ok(bytes)
}
fn read_persisted_video_task_store(path: &Path) -> std::io::Result<PersistedVideoTaskStore> {
match std::fs::read(path) {
Ok(bytes) => Ok(PersistedVideoTaskStore::Bytes(bytes)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Ok(PersistedVideoTaskStore::Missing)
}
Err(error) => Err(error),
}
}
fn replace_video_task_store_if_unchanged(
path: &Path,
expected: &PersistedVideoTaskStore,
replacement: &[u8],
) -> std::io::Result<()> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent)?;
}
let lock_path = video_task_store_lock_path(path);
let lock_file = open_private_lock_file(&lock_path)?;
// Lock a stable sidecar inode: the store inode itself is replaced by rename.
lock_file.lock()?;
// The bytes captured before parsing/decryption are the migration/write CAS token.
let observed = read_persisted_video_task_store(path)?;
if &observed != expected {
return Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"video task store changed before compare-and-replace",
));
}
let temp_path = path.with_extension(format!("tmp-{}", Uuid::new_v4()));
if let Err(error) = write_private_file(&temp_path, replacement) {
let _ = std::fs::remove_file(&temp_path);
return Err(error);
}
if let Err(error) = std::fs::rename(&temp_path, path) {
let _ = std::fs::remove_file(&temp_path);
return Err(error);
}
Ok(())
}
fn video_task_store_lock_path(path: &Path) -> PathBuf {
let mut lock_path = path.as_os_str().to_os_string();
lock_path.push(".lock");
PathBuf::from(lock_path)
}
fn open_private_lock_file(path: &Path) -> std::io::Result<std::fs::File> {
let mut options = std::fs::OpenOptions::new();
options.read(true).write(true).create(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
options.open(path)
}
fn write_private_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
impl VideoTaskStore for FileVideoTaskStore {
fn insert(&self, snapshot: LocalVideoTaskSnapshot) {
let _ = self.mutate_registry(|registry| {
@@ -169,3 +383,283 @@ impl VideoTaskStore for FileVideoTaskStore {
self.mutate_registry(|registry| registry.project_gemini(short_id, provider_body))
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
use serde_json::json;
use super::*;
use crate::{LocalVideoTaskPersistence, LocalVideoTaskStatus, LocalVideoTaskTransport};
fn temp_store_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"aether-video-store-{name}-{}-{}.json",
std::process::id(),
Uuid::new_v4()
))
}
fn cleanup_store_path(path: &Path) {
std::fs::remove_file(path).ok();
std::fs::remove_file(video_task_store_lock_path(path)).ok();
}
fn sensitive_gemini_snapshot() -> LocalVideoTaskSnapshot {
LocalVideoTaskSnapshot::Gemini(GeminiVideoTaskSeed {
local_short_id: "task-sensitive".to_string(),
upstream_operation_name: "operations/upstream-sensitive".to_string(),
user_id: Some("user-1".to_string()),
api_key_id: Some("api-key-1".to_string()),
model: "veo-3".to_string(),
status: LocalVideoTaskStatus::Failed,
progress_percent: 100,
error_code: Some("Bearer code-secret".to_string()),
error_message: Some("Authorization: Bearer error-secret".to_string()),
metadata: json!({
"debug": "metadata-secret",
"url": "https://internal.test/result?token=metadata-query-secret"
}),
persistence: LocalVideoTaskPersistence {
request_id: "request-1".to_string(),
username: Some("alice".to_string()),
api_key_name: Some("primary".to_string()),
client_api_format: "gemini:video".to_string(),
provider_api_format: "gemini:video".to_string(),
original_request_body: json!({"prompt": "create a video"}),
format_converted: false,
},
transport: LocalVideoTaskTransport {
upstream_base_url: "https://generativelanguage.googleapis.com".to_string(),
provider_name: Some("gemini".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
headers: BTreeMap::from([(
"x-goog-api-key".to_string(),
"transport-key-required-for-resume".to_string(),
)]),
content_type: Some("application/json".to_string()),
model_name: Some("veo-3".to_string()),
proxy: None,
transport_profile: None,
timeouts: None,
},
})
}
#[test]
fn loading_encrypted_store_rewrites_legacy_provider_diagnostics() {
let path = temp_store_path("diagnostic-migration");
let legacy_plaintext = serde_json::to_string(&json!({
"openai": {},
"gemini": {
"task-sensitive": sensitive_gemini_snapshot()
}
}))
.expect("legacy registry should serialize");
let ciphertext =
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, &legacy_plaintext)
.expect("legacy registry should encrypt");
std::fs::write(
&path,
format!("{LEGACY_ENCRYPTED_VIDEO_TASK_STORE_PREFIX}{ciphertext}\n"),
)
.expect("legacy encrypted registry should be written");
let store = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect("legacy encrypted registry should load");
let response = store
.read_gemini("task-sensitive")
.expect("migrated task should remain readable");
assert_eq!(response.body_json["error"]["code"], "provider_error");
assert_eq!(
response.body_json["error"]["message"],
"Video generation failed"
);
let bytes = std::fs::read(&path).expect("migrated registry should be readable");
let ciphertext = bytes
.strip_prefix(ENCRYPTED_VIDEO_TASK_STORE_PREFIX.as_bytes())
.expect("migrated registry should stay encrypted");
let ciphertext = std::str::from_utf8(ciphertext)
.expect("ciphertext should be UTF-8")
.trim();
let migrated_plaintext =
decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, ciphertext)
.expect("migrated registry should decrypt");
for secret in [
"code-secret",
"error-secret",
"metadata-secret",
"metadata-query-secret",
] {
assert!(
!migrated_plaintext.contains(secret),
"migrated registry leaked {secret}"
);
}
assert!(migrated_plaintext.contains("transport-key-required-for-resume"));
assert!(migrated_plaintext.contains("provider_error"));
cleanup_store_path(&path);
}
#[test]
fn rejects_plaintext_registry_without_rewriting_it() {
let path = temp_store_path("plaintext-injection");
let plaintext = serde_json::to_vec(&json!({
"openai": {},
"gemini": {},
}))
.expect("plaintext registry should serialize");
std::fs::write(&path, &plaintext).expect("plaintext registry should be written");
let error = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect_err("plaintext registry must fail closed");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("plaintext"));
assert_eq!(
std::fs::read(&path).expect("rejected registry should remain readable"),
plaintext,
"rejected plaintext must not be rewritten into an authenticated envelope",
);
cleanup_store_path(&path);
}
#[test]
fn rejects_unknown_aether_envelopes_without_rewriting_them() {
for (name, bytes) in [
(
"unknown-video-version",
b"aether-video-tasks-v999\nopaque".as_slice(),
),
(
"foreign-aether-envelope",
b"aether-other-v1\nopaque".as_slice(),
),
] {
let path = temp_store_path(name);
std::fs::write(&path, bytes).expect("unknown envelope should be written");
let error = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect_err("unknown aether envelope must fail closed");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("unsupported"));
assert_eq!(
std::fs::read(&path).expect("rejected envelope should remain readable"),
bytes,
);
cleanup_store_path(&path);
}
}
#[test]
fn rejects_tampered_authenticated_store_without_rewriting_it() {
let path = temp_store_path("tampered-v2");
let mut bytes = encrypted_video_task_store_bytes(
DEVELOPMENT_ENCRYPTION_KEY,
&VideoTaskRegistry::default(),
)
.expect("encrypted registry should serialize");
let tampered_index = ENCRYPTED_VIDEO_TASK_STORE_PREFIX.len() + 12;
bytes[tampered_index] = if bytes[tampered_index] == b'A' {
b'B'
} else {
b'A'
};
std::fs::write(&path, &bytes).expect("tampered registry should be written");
let error = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect_err("tampered authenticated registry must fail closed");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("decryption failed"));
assert_eq!(
std::fs::read(&path).expect("tampered registry should remain readable"),
bytes,
);
cleanup_store_path(&path);
}
#[test]
fn legacy_migration_compare_before_replace_preserves_concurrent_replacement() {
let path = temp_store_path("legacy-migration-race");
let legacy_plaintext = serde_json::to_string(&json!({
"openai": {},
"gemini": {
"task-sensitive": sensitive_gemini_snapshot(),
},
}))
.expect("legacy registry should serialize");
let legacy_ciphertext =
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, &legacy_plaintext)
.expect("legacy registry should encrypt");
let legacy_bytes =
format!("{LEGACY_ENCRYPTED_VIDEO_TASK_STORE_PREFIX}{legacy_ciphertext}\n").into_bytes();
std::fs::write(&path, &legacy_bytes).expect("legacy registry should be written");
let loaded = FileVideoTaskStore::load_registry(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect("authenticated legacy registry should load");
assert!(loaded.needs_rewrite);
assert_eq!(
loaded.persisted_file,
PersistedVideoTaskStore::Bytes(legacy_bytes),
);
let store = FileVideoTaskStore {
path: path.clone(),
encryption_key: DEVELOPMENT_ENCRYPTION_KEY.to_string(),
registry: Mutex::new(loaded.registry),
persisted_file: Mutex::new(loaded.persisted_file),
};
let concurrent_replacement = encrypted_video_task_store_bytes(
DEVELOPMENT_ENCRYPTION_KEY,
&VideoTaskRegistry::default(),
)
.expect("replacement registry should encrypt");
std::fs::write(&path, &concurrent_replacement)
.expect("concurrent replacement should be written");
let registry = store.registry.lock().expect("registry lock should succeed");
let error = store
.persist_registry(&registry)
.expect_err("stale legacy migration must report a conflict");
drop(registry);
assert_eq!(error.kind(), std::io::ErrorKind::WouldBlock);
assert_eq!(
std::fs::read(&path).expect("concurrent replacement should remain readable"),
concurrent_replacement,
"stale migration must not overwrite a newer exact byte snapshot",
);
cleanup_store_path(&path);
}
#[test]
fn rejects_valid_fernet_ciphertext_from_another_purpose() {
let path = temp_store_path("purpose-mismatch");
let protected = format!(
"another-purpose\0{}",
serde_json::to_string(&VideoTaskRegistry::default())
.expect("empty registry should serialize")
);
let ciphertext = encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, &protected)
.expect("foreign payload should encrypt");
std::fs::write(
&path,
format!("{ENCRYPTED_VIDEO_TASK_STORE_PREFIX}{ciphertext}\n"),
)
.expect("foreign ciphertext should be written");
let error = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY)
.expect_err("foreign-purpose ciphertext must fail closed");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
cleanup_store_path(&path);
}
}