v0.3.5: Private blob encryption, blob prefetch, intent-based filtering, crypto refactoring

Private blob encryption:
- Encrypted posts (Friends/Circle/Direct) now encrypt attachment blobs with same CEK
- Public blobs unchanged, CID computed on ciphertext for private
- decrypt_blob_for_post/get_blob_for_post for transparent decryption on retrieval

Blob prefetch:
- Pull cycle and sync_with eagerly fetch missing blobs after post sync
- prefetch_blobs_from_peer scans for missing attachments, fetches via fallback chain
- Runs outside conn_mgr lock at Node level

Crypto refactoring:
- Extracted: encrypt/decrypt_bytes_with_cek, wrap/unwrap_cek_for_recipients
- unwrap_cek_for_recipient, unwrap_group_cek, random_cek
- encrypt_post_with_cek, encrypt_post_for_group_with_cek variants
- All existing functions refactored to delegate, 19 crypto tests pass

Intent-based filtering:
- intent_kind field on PostDto ("public"/"friends"/"circle"/"direct"/"unknown")
- Feed/MyPosts filter on intentKind !== 'direct' instead of visibility
- Messages filter with backward-compatible fallback for pre-intent posts
- get_post_intent storage method

IPC updates:
- resolve_blob_data helper using get_blob_for_post with network fallback
- sanitize_download_filename prevents path traversal
- get_blob_path accepts optional post_id_hex

Website:
- Mobile hamburger nav on all pages
- Mesh/Non-mesh N1 labels in network diagnostics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-20 12:44:07 -04:00
parent 0abc244ee9
commit a41b11c0b8
14 changed files with 562 additions and 325 deletions

View file

@ -644,6 +644,23 @@ impl Storage {
}
}
/// Get the visibility intent for a post (if stored).
pub fn get_post_intent(&self, id: &PostId) -> anyhow::Result<Option<VisibilityIntent>> {
let mut stmt = self.conn.prepare(
"SELECT visibility_intent FROM posts WHERE id = ?1",
)?;
let mut rows = stmt.query(params![id.as_slice()])?;
if let Some(row) = rows.next()? {
let intent_json: Option<String> = row.get(0)?;
match intent_json {
Some(json) => Ok(serde_json::from_str(&json).ok()),
None => Ok(None),
}
} else {
Ok(None)
}
}
pub fn list_post_ids(&self) -> anyhow::Result<Vec<PostId>> {
let mut stmt = self.conn.prepare("SELECT id FROM posts")?;
let rows = stmt.query_map([], |row| {