v0.3.5: Encrypted receipt & comment slots, message delivery indicators
Encrypted slots in BlobHeader:
- Private posts get noise-prefilled receipt slots (64B, 1 per participant)
and comment slots (256B, ceil(participants/3), expandable)
- Slot key derived from post CEK via BLAKE3 — only participants can read
- CDN relays propagate opaque encrypted bytes without decryption
- 3 new BlobHeaderDiffOps: WriteReceiptSlot, WriteCommentSlot, AddCommentSlots
Receipt system:
- States: empty(0), delivered(1), seen(2), reacted(3)
- Slot index = position in sorted participant NodeId list
- Author can pre-feed emoji reaction at creation time
- 6 new crypto tests for slot encrypt/decrypt/derivation
Node methods:
- write_receipt_slot, write_comment_slot with upstream+downstream propagation
- read_receipt_slots, read_comment_slots with CEK-based decryption
- get_post_cek_and_participants helper for both Encrypted and GroupEncrypted
IPC: write_message_receipt, write_message_comment, get_message_receipts,
get_message_comments
Frontend:
- DM chat bubbles show delivery indicators (check → double → blue → emoji)
- Opening conversation auto-marks incoming messages as seen
- React button on messages with emoji prompt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a41b11c0b8
commit
b7f2d369fa
9 changed files with 882 additions and 4 deletions
|
|
@ -517,6 +517,34 @@ pub fn re_encrypt_post(
|
|||
encrypt_post(&plaintext, our_seed, our_node_id, new_recipient_ids)
|
||||
}
|
||||
|
||||
// --- Slot encryption (receipt + comment slots for encrypted posts) ---
|
||||
|
||||
const SLOT_KEY_CONTEXT: &str = "itsgoin/slot/v1";
|
||||
|
||||
/// Derive the slot encryption key from a post's CEK.
|
||||
/// Only participants who can unwrap the CEK can derive this key.
|
||||
pub fn derive_slot_key(cek: &[u8; 32]) -> [u8; 32] {
|
||||
blake3::derive_key(SLOT_KEY_CONTEXT, cek)
|
||||
}
|
||||
|
||||
/// Encrypt a slot's plaintext bytes using the slot key (derived from CEK).
|
||||
/// Returns encrypted payload via encrypt_bytes_with_cek.
|
||||
pub fn encrypt_slot(plaintext: &[u8], slot_key: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
encrypt_bytes_with_cek(plaintext, slot_key)
|
||||
}
|
||||
|
||||
/// Decrypt a slot's encrypted bytes using the slot key (derived from CEK).
|
||||
pub fn decrypt_slot(encrypted: &[u8], slot_key: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
decrypt_bytes_with_cek(encrypted, slot_key)
|
||||
}
|
||||
|
||||
/// Generate a random noise-filled slot (indistinguishable from encrypted data).
|
||||
pub fn random_slot_noise(size: usize) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::rng().fill_bytes(&mut buf);
|
||||
buf
|
||||
}
|
||||
|
||||
// --- Engagement crypto ---
|
||||
|
||||
const REACTION_WRAP_CONTEXT: &str = "itsgoin/private-reaction/v1";
|
||||
|
|
@ -970,4 +998,84 @@ mod tests {
|
|||
let result = decrypt_group_post(&encrypted_v2, &group_seed_v1, &group_pubkey_v1, &wrapped_cek_v2);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_key_derivation_deterministic() {
|
||||
let cek = [42u8; 32];
|
||||
let key1 = derive_slot_key(&cek);
|
||||
let key2 = derive_slot_key(&cek);
|
||||
assert_eq!(key1, key2);
|
||||
// Different CEK gives different slot key
|
||||
let cek2 = [99u8; 32];
|
||||
let key3 = derive_slot_key(&cek2);
|
||||
assert_ne!(key1, key3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_encrypt_decrypt_roundtrip() {
|
||||
let cek = [42u8; 32];
|
||||
let slot_key = derive_slot_key(&cek);
|
||||
let plaintext = b"hello slot encryption";
|
||||
let encrypted = encrypt_slot(plaintext, &slot_key).unwrap();
|
||||
let decrypted = decrypt_slot(&encrypted, &slot_key).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_wrong_key_fails() {
|
||||
let cek = [42u8; 32];
|
||||
let slot_key = derive_slot_key(&cek);
|
||||
let wrong_key = derive_slot_key(&[99u8; 32]);
|
||||
let plaintext = b"secret data";
|
||||
let encrypted = encrypt_slot(plaintext, &slot_key).unwrap();
|
||||
assert!(decrypt_slot(&encrypted, &wrong_key).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_slot_roundtrip() {
|
||||
let cek = [42u8; 32];
|
||||
let slot_key = derive_slot_key(&cek);
|
||||
|
||||
// Build a receipt: state=seen, timestamp, no emoji
|
||||
let mut plaintext = [0u8; 32];
|
||||
plaintext[0] = 2; // seen
|
||||
let ts: u64 = 1700000000000;
|
||||
plaintext[1..9].copy_from_slice(&ts.to_le_bytes());
|
||||
|
||||
let encrypted = encrypt_slot(&plaintext, &slot_key).unwrap();
|
||||
let decrypted = decrypt_slot(&encrypted, &slot_key).unwrap();
|
||||
assert_eq!(decrypted[0], 2);
|
||||
assert_eq!(u64::from_le_bytes(decrypted[1..9].try_into().unwrap()), ts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comment_slot_roundtrip() {
|
||||
let cek = [42u8; 32];
|
||||
let slot_key = derive_slot_key(&cek);
|
||||
|
||||
let mut plaintext = [0u8; 256];
|
||||
let author = [1u8; 32];
|
||||
plaintext[..32].copy_from_slice(&author);
|
||||
let ts: u64 = 1700000000000;
|
||||
plaintext[32..40].copy_from_slice(&ts.to_le_bytes());
|
||||
let content = b"Hello from a slot comment!";
|
||||
plaintext[40..40 + content.len()].copy_from_slice(content);
|
||||
|
||||
let encrypted = encrypt_slot(&plaintext, &slot_key).unwrap();
|
||||
let decrypted = decrypt_slot(&encrypted, &slot_key).unwrap();
|
||||
assert_eq!(&decrypted[..32], &author);
|
||||
assert_eq!(u64::from_le_bytes(decrypted[32..40].try_into().unwrap()), ts);
|
||||
let end = decrypted[40..].iter().position(|&b| b == 0).unwrap_or(216);
|
||||
assert_eq!(&decrypted[40..40 + end], content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_slot_noise_correct_size() {
|
||||
let noise64 = random_slot_noise(64);
|
||||
assert_eq!(noise64.len(), 64);
|
||||
let noise256 = random_slot_noise(256);
|
||||
assert_eq!(noise256.len(), 256);
|
||||
// Different calls produce different noise (with very high probability)
|
||||
assert_ne!(random_slot_noise(64), random_slot_noise(64));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue