v0.0.6: Delivery receipts (sent/delivered/read)
Protocol:
- WireMessage::Receipt { sender_fingerprint, message_id, receipt_type }
- ReceiptType enum: Delivered, Read
- id field added to KeyExchange and Message variants
- Receipts are plaintext (not encrypted) — contain only ID + type
Web client:
- Auto-sends Delivered receipt on successful decrypt
- Tracks sent message IDs with receipt status
- Displays: ✓ (sent, gray), ✓✓ (delivered, white), ✓✓ (read, blue)
- Receipt indicators update live via DOM reference
CLI TUI:
- Auto-sends Delivered receipt back to sender on decrypt
- Tracks receipt status per message ID
- Displays receipt indicators after sent messages
WASM:
- create_receipt() function for web client
- encrypt_with_id/encrypt_key_exchange_with_id for tracking
- decrypt_wire_message handles Receipt variant
17/17 protocol tests pass. Zero warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use warzone_protocol::identity::{IdentityKeyPair, PublicIdentity, Seed};
|
||||
use warzone_protocol::message::WireMessage;
|
||||
use warzone_protocol::message::{ReceiptType, WireMessage};
|
||||
use warzone_protocol::prekey::{
|
||||
generate_signed_pre_key, PreKeyBundle,
|
||||
};
|
||||
@@ -155,6 +155,16 @@ impl WasmSession {
|
||||
identity: &WasmIdentity,
|
||||
their_bundle_bytes: &[u8],
|
||||
plaintext: &str,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
self.encrypt_key_exchange_with_id(identity, their_bundle_bytes, plaintext, &uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
|
||||
pub fn encrypt_key_exchange_with_id(
|
||||
&mut self,
|
||||
identity: &WasmIdentity,
|
||||
their_bundle_bytes: &[u8],
|
||||
plaintext: &str,
|
||||
msg_id: &str,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let bundle: PreKeyBundle = bincode::deserialize(their_bundle_bytes)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
@@ -165,6 +175,7 @@ impl WasmSession {
|
||||
.map_err(|e| JsValue::from_str(&format!("encrypt: {}", e)))?;
|
||||
|
||||
let wire = WireMessage::KeyExchange {
|
||||
id: msg_id.to_string(),
|
||||
sender_fingerprint: identity.pub_id.fingerprint.to_string(),
|
||||
sender_identity_encryption_key: *identity.pub_id.encryption.as_bytes(),
|
||||
ephemeral_public: *result.ephemeral_public.as_bytes(),
|
||||
@@ -175,9 +186,14 @@ impl WasmSession {
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, identity: &WasmIdentity, plaintext: &str) -> Result<Vec<u8>, JsValue> {
|
||||
self.encrypt_with_id(identity, plaintext, &uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
|
||||
pub fn encrypt_with_id(&mut self, identity: &WasmIdentity, plaintext: &str, msg_id: &str) -> Result<Vec<u8>, JsValue> {
|
||||
let encrypted = self.ratchet.encrypt(plaintext.as_bytes())
|
||||
.map_err(|e| JsValue::from_str(&format!("encrypt: {}", e)))?;
|
||||
let wire = WireMessage::Message {
|
||||
id: msg_id.to_string(),
|
||||
sender_fingerprint: identity.pub_id.fingerprint.to_string(),
|
||||
ratchet_message: encrypted,
|
||||
};
|
||||
@@ -198,6 +214,30 @@ impl WasmSession {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Receipt creation ──
|
||||
|
||||
/// Create a Receipt wire message (plaintext, not encrypted).
|
||||
/// `receipt_type`: "delivered" or "read".
|
||||
/// Returns bincode-serialized bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn create_receipt(
|
||||
sender_fingerprint: &str,
|
||||
message_id: &str,
|
||||
receipt_type: &str,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let rt = match receipt_type {
|
||||
"delivered" => ReceiptType::Delivered,
|
||||
"read" => ReceiptType::Read,
|
||||
_ => return Err(JsValue::from_str("receipt_type must be 'delivered' or 'read'")),
|
||||
};
|
||||
let wire = WireMessage::Receipt {
|
||||
sender_fingerprint: sender_fingerprint.to_string(),
|
||||
message_id: message_id.to_string(),
|
||||
receipt_type: rt,
|
||||
};
|
||||
bincode::serialize(&wire).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
// ── Self-test (verifies full encrypt/decrypt cycle within WASM) ──
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -241,6 +281,7 @@ pub fn self_test() -> Result<String, JsValue> {
|
||||
let encrypted_clone = encrypted.clone();
|
||||
|
||||
let _wire = WireMessage::KeyExchange {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
sender_fingerprint: alice_pub.fingerprint.to_string(),
|
||||
sender_identity_encryption_key: *alice_pub.encryption.as_bytes(),
|
||||
ephemeral_public: *x3dh_result.ephemeral_public.as_bytes(),
|
||||
@@ -304,7 +345,8 @@ pub fn debug_bundle_info(identity: &mut WasmIdentity) -> Result<String, JsValue>
|
||||
|
||||
/// Decrypt a bincode WireMessage. `spk_secret_hex` is the signed pre-key secret
|
||||
/// (stored in localStorage, generated during identity creation).
|
||||
/// Returns JSON: { "sender": "fp", "text": "...", "new_session": bool, "session_data": "base64..." }
|
||||
/// Returns JSON: { "sender": "fp", "text": "...", "new_session": bool, "session_data": "base64...", "message_id": "..." }
|
||||
/// For Receipt messages: { "type": "receipt", "sender": "fp", "message_id": "...", "receipt_type": "delivered"|"read" }
|
||||
#[wasm_bindgen]
|
||||
pub fn decrypt_wire_message(
|
||||
identity_hex_seed: &str,
|
||||
@@ -324,6 +366,7 @@ pub fn decrypt_wire_message(
|
||||
|
||||
match wire {
|
||||
WireMessage::KeyExchange {
|
||||
id: msg_id,
|
||||
sender_fingerprint,
|
||||
sender_identity_encryption_key,
|
||||
ephemeral_public,
|
||||
@@ -357,9 +400,11 @@ pub fn decrypt_wire_message(
|
||||
"text": String::from_utf8_lossy(&plain),
|
||||
"new_session": true,
|
||||
"session_data": session_b64,
|
||||
"message_id": msg_id,
|
||||
}).to_string())
|
||||
}
|
||||
WireMessage::Message {
|
||||
id: msg_id,
|
||||
sender_fingerprint,
|
||||
ratchet_message,
|
||||
} => {
|
||||
@@ -384,6 +429,23 @@ pub fn decrypt_wire_message(
|
||||
"text": String::from_utf8_lossy(&plain),
|
||||
"new_session": false,
|
||||
"session_data": session_b64,
|
||||
"message_id": msg_id,
|
||||
}).to_string())
|
||||
}
|
||||
WireMessage::Receipt {
|
||||
sender_fingerprint,
|
||||
message_id,
|
||||
receipt_type,
|
||||
} => {
|
||||
let rt_str = match receipt_type {
|
||||
ReceiptType::Delivered => "delivered",
|
||||
ReceiptType::Read => "read",
|
||||
};
|
||||
Ok(serde_json::json!({
|
||||
"type": "receipt",
|
||||
"sender": sender_fingerprint,
|
||||
"message_id": message_id,
|
||||
"receipt_type": rt_str,
|
||||
}).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user