mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
709 lines
26 KiB
Rust
709 lines
26 KiB
Rust
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(Default)]
|
|
pub struct InMemoryVideoTaskStore {
|
|
registry: Mutex<VideoTaskRegistry>,
|
|
}
|
|
|
|
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() {
|
|
registry.insert(snapshot);
|
|
}
|
|
}
|
|
|
|
fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.read_openai(task_id)
|
|
}
|
|
|
|
fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.read_gemini(short_id)
|
|
}
|
|
|
|
fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.clone_openai(task_id)
|
|
}
|
|
|
|
fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.clone_gemini(short_id)
|
|
}
|
|
|
|
fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot> {
|
|
let Ok(registry) = self.registry.lock() else {
|
|
return Vec::new();
|
|
};
|
|
registry.list_active_snapshots(limit)
|
|
}
|
|
|
|
fn apply_mutation(&self, mutation: LocalVideoTaskRegistryMutation) {
|
|
if let Ok(mut registry) = self.registry.lock() {
|
|
registry.apply_mutation(mutation);
|
|
}
|
|
}
|
|
|
|
fn project_openai(&self, task_id: &str, provider_body: &Map<String, Value>) -> bool {
|
|
let Ok(mut registry) = self.registry.lock() else {
|
|
return false;
|
|
};
|
|
registry.project_openai(task_id, provider_body)
|
|
}
|
|
|
|
fn project_gemini(&self, short_id: &str, provider_body: &Map<String, Value>) -> bool {
|
|
let Ok(mut registry) = self.registry.lock() else {
|
|
return false;
|
|
};
|
|
registry.project_gemini(short_id, provider_body)
|
|
}
|
|
}
|
|
|
|
impl FileVideoTaskStore {
|
|
pub fn new(
|
|
path: impl Into<PathBuf>,
|
|
encryption_key: impl Into<String>,
|
|
) -> std::io::Result<Self> {
|
|
let path = path.into();
|
|
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(®istry)?;
|
|
}
|
|
Ok(store)
|
|
}
|
|
|
|
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 Err(invalid_store_data("video task store is empty"));
|
|
}
|
|
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<()> {
|
|
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(())
|
|
}
|
|
|
|
fn mutate_registry(&self, mutator: impl FnOnce(&mut VideoTaskRegistry) -> bool) -> bool {
|
|
let Ok(mut registry) = self.registry.lock() else {
|
|
return false;
|
|
};
|
|
let mut updated_registry = registry.clone();
|
|
if !mutator(&mut updated_registry) {
|
|
return false;
|
|
}
|
|
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| {
|
|
registry.insert(snapshot);
|
|
true
|
|
});
|
|
}
|
|
|
|
fn read_openai(&self, task_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.read_openai(task_id)
|
|
}
|
|
|
|
fn read_gemini(&self, short_id: &str) -> Option<LocalVideoTaskReadResponse> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.read_gemini(short_id)
|
|
}
|
|
|
|
fn clone_openai(&self, task_id: &str) -> Option<OpenAiVideoTaskSeed> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.clone_openai(task_id)
|
|
}
|
|
|
|
fn clone_gemini(&self, short_id: &str) -> Option<GeminiVideoTaskSeed> {
|
|
let registry = self.registry.lock().ok()?;
|
|
registry.clone_gemini(short_id)
|
|
}
|
|
|
|
fn list_active_snapshots(&self, limit: usize) -> Vec<LocalVideoTaskSnapshot> {
|
|
let Ok(registry) = self.registry.lock() else {
|
|
return Vec::new();
|
|
};
|
|
registry.list_active_snapshots(limit)
|
|
}
|
|
|
|
fn apply_mutation(&self, mutation: LocalVideoTaskRegistryMutation) {
|
|
let _ = self.mutate_registry(|registry| {
|
|
registry.apply_mutation(mutation);
|
|
true
|
|
});
|
|
}
|
|
|
|
fn project_openai(&self, task_id: &str, provider_body: &Map<String, Value>) -> bool {
|
|
self.mutate_registry(|registry| registry.project_openai(task_id, provider_body))
|
|
}
|
|
|
|
fn project_gemini(&self, short_id: &str, provider_body: &Map<String, Value>) -> bool {
|
|
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 completed_gemini_download_survives_encrypted_file_reload() {
|
|
let path = temp_store_path("completed-download");
|
|
let video_url = "https://cdn.example.test/video.mp4?signature=a%2Fb%2Bc%3D";
|
|
let mut snapshot = sensitive_gemini_snapshot();
|
|
snapshot.apply_provider_body(
|
|
json!({
|
|
"done": true,
|
|
"debug": "private-debug",
|
|
"response": {
|
|
"generateVideoResponse": {
|
|
"generatedSamples": [{"video": {"uri": video_url}}]
|
|
}
|
|
}
|
|
})
|
|
.as_object()
|
|
.expect("provider body"),
|
|
);
|
|
let store = FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY).expect("store");
|
|
store.insert(snapshot);
|
|
drop(store);
|
|
let bytes = std::fs::read_to_string(&path).expect("encrypted store file");
|
|
assert!(bytes.starts_with(ENCRYPTED_VIDEO_TASK_STORE_PREFIX));
|
|
assert!(!bytes.contains(video_url));
|
|
assert!(!bytes.contains("transport-key-required-for-resume"));
|
|
let restored =
|
|
FileVideoTaskStore::new(&path, DEVELOPMENT_ENCRYPTION_KEY).expect("restored store");
|
|
let task = restored
|
|
.clone_gemini("task-sensitive")
|
|
.expect("completed Gemini task");
|
|
let record = task.to_upsert_record();
|
|
assert_eq!(
|
|
record.status,
|
|
aether_data_contracts::repository::video_tasks::VideoTaskStatus::Completed
|
|
);
|
|
assert_eq!(record.video_url.as_deref(), Some(video_url));
|
|
assert_eq!(record.prompt.as_deref(), Some("create a video"));
|
|
assert!(!task.metadata.to_string().contains("private-debug"));
|
|
assert!(record.request_metadata.is_none());
|
|
drop(restored);
|
|
cleanup_store_path(&path);
|
|
}
|
|
|
|
#[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(®istry)
|
|
.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);
|
|
}
|
|
}
|