void_core/collab/
invite.rs1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
13use serde::{Deserialize, Serialize};
14
15use super::manifest::{SigningPubKey, WrappedKey};
16
17pub const INVITE_TYPE_V1: &str = "void-invite/v1";
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Invite {
27 #[serde(rename = "type")]
29 pub invite_type: String,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub repo_name: Option<String>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub repo_id: Option<String>,
38
39 pub head_cid: String,
41
42 pub wrapped_key: WrappedKey,
44
45 pub for_recipient: SigningPubKey,
47
48 pub from_owner: SigningPubKey,
50
51 pub created_at: u64,
53
54 #[serde(with = "signature_base64")]
56 pub signature: Vec<u8>,
57}
58
59impl Invite {
60 pub fn signable_bytes(&self) -> Vec<u8> {
65 let mut buf = Vec::with_capacity(256);
66
67 append_field(&mut buf, self.invite_type.as_bytes());
69 append_field(&mut buf, self.head_cid.as_bytes());
71 append_field(&mut buf, self.wrapped_key.as_bytes());
73 append_field(&mut buf, self.for_recipient.as_bytes());
75 append_field(&mut buf, self.from_owner.as_bytes());
77 buf.extend_from_slice(&self.created_at.to_le_bytes());
79
80 match &self.repo_name {
82 Some(name) => {
83 buf.push(1);
84 append_field(&mut buf, name.as_bytes());
85 }
86 None => buf.push(0),
87 }
88 match &self.repo_id {
89 Some(id) => {
90 buf.push(1);
91 append_field(&mut buf, id.as_bytes());
92 }
93 None => buf.push(0),
94 }
95
96 buf
97 }
98
99 pub fn sign(&mut self, signing_key: &SigningKey) {
101 let signable = self.signable_bytes();
102 let sig: Signature = signing_key.sign(&signable);
103 self.signature = sig.to_bytes().to_vec();
104 }
105
106 pub fn verify(&self) -> bool {
110 if self.signature.len() != 64 {
111 return false;
112 }
113
114 let Ok(verifying_key) = VerifyingKey::from_bytes(self.from_owner.as_bytes()) else {
115 return false;
116 };
117
118 let mut sig_bytes = [0u8; 64];
119 sig_bytes.copy_from_slice(&self.signature);
120 let signature = Signature::from_bytes(&sig_bytes);
121
122 let signable = self.signable_bytes();
123 verifying_key.verify(&signable, &signature).is_ok()
124 }
125}
126
127pub fn is_invite_blob(data: &[u8]) -> bool {
133 let trimmed = match data.iter().position(|&b| !b.is_ascii_whitespace()) {
135 Some(pos) => &data[pos..],
136 None => return false,
137 };
138 if !trimmed.starts_with(b"{") {
139 return false;
140 }
141
142 let Ok(value) = serde_json::from_slice::<serde_json::Value>(data) else {
144 return false;
145 };
146
147 value
148 .get("type")
149 .and_then(|v| v.as_str())
150 .map_or(false, |t| t == INVITE_TYPE_V1)
151}
152
153pub fn parse_invite(data: &[u8]) -> Option<Invite> {
155 if !is_invite_blob(data) {
156 return None;
157 }
158 serde_json::from_slice(data).ok()
159}
160
161fn append_field(buf: &mut Vec<u8>, data: &[u8]) {
163 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
164 buf.extend_from_slice(data);
165}
166
167mod signature_base64 {
172 use base64::{engine::general_purpose::STANDARD, Engine};
173 use serde::{Deserialize, Deserializer, Serializer};
174
175 pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
176 where
177 S: Serializer,
178 {
179 serializer.serialize_str(&STANDARD.encode(bytes))
180 }
181
182 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
183 where
184 D: Deserializer<'de>,
185 {
186 let s = String::deserialize(deserializer)?;
187 STANDARD.decode(s).map_err(serde::de::Error::custom)
188 }
189}
190
191#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn make_test_invite() -> (Invite, SigningKey) {
200 let signing_key = SigningKey::generate(&mut rand::thread_rng());
201 let owner_pub = SigningPubKey::from_bytes(signing_key.verifying_key().to_bytes());
202 let recipient_pub = SigningPubKey::from_bytes([0xbb; 32]);
203
204 let invite = Invite {
205 invite_type: INVITE_TYPE_V1.to_string(),
206 repo_name: Some("test-repo".to_string()),
207 repo_id: Some("uuid-1234".to_string()),
208 head_cid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string(),
209 wrapped_key: WrappedKey::from_bytes(vec![0xaa; 92]),
210 for_recipient: recipient_pub,
211 from_owner: owner_pub,
212 created_at: 1700000000,
213 signature: vec![],
214 };
215
216 (invite, signing_key)
217 }
218
219 #[test]
220 fn sign_and_verify_roundtrip() {
221 let (mut invite, signing_key) = make_test_invite();
222
223 invite.sign(&signing_key);
224 assert_eq!(invite.signature.len(), 64);
225 assert!(invite.verify());
226 }
227
228 #[test]
229 fn verify_fails_with_wrong_key() {
230 let (mut invite, signing_key) = make_test_invite();
231 invite.sign(&signing_key);
232
233 invite.from_owner = SigningPubKey::from_bytes([0xcc; 32]);
235 assert!(!invite.verify());
236 }
237
238 #[test]
239 fn verify_fails_with_tampered_data() {
240 let (mut invite, signing_key) = make_test_invite();
241 invite.sign(&signing_key);
242
243 invite.head_cid = "bafytampered".to_string();
245 assert!(!invite.verify());
246 }
247
248 #[test]
249 fn verify_fails_with_empty_signature() {
250 let (invite, _) = make_test_invite();
251 assert!(!invite.verify());
252 }
253
254 #[test]
255 fn json_roundtrip() {
256 let (mut invite, signing_key) = make_test_invite();
257 invite.sign(&signing_key);
258
259 let json = serde_json::to_string_pretty(&invite).unwrap();
260 let parsed: Invite = serde_json::from_str(&json).unwrap();
261
262 assert_eq!(parsed.invite_type, INVITE_TYPE_V1);
263 assert_eq!(parsed.head_cid, invite.head_cid);
264 assert_eq!(parsed.repo_name, invite.repo_name);
265 assert_eq!(parsed.for_recipient.as_bytes(), invite.for_recipient.as_bytes());
266 assert_eq!(parsed.from_owner.as_bytes(), invite.from_owner.as_bytes());
267 assert!(parsed.verify());
268 }
269
270 #[test]
271 fn is_invite_blob_detects_valid() {
272 let (mut invite, signing_key) = make_test_invite();
273 invite.sign(&signing_key);
274
275 let json = serde_json::to_vec(&invite).unwrap();
276 assert!(is_invite_blob(&json));
277 }
278
279 #[test]
280 fn is_invite_blob_rejects_non_json() {
281 assert!(!is_invite_blob(b"not json"));
282 assert!(!is_invite_blob(b""));
283 assert!(!is_invite_blob(&[0x00, 0x01, 0x02]));
284 }
285
286 #[test]
287 fn is_invite_blob_rejects_wrong_type() {
288 let json = br#"{"type": "something-else"}"#;
289 assert!(!is_invite_blob(json));
290 }
291
292 #[test]
293 fn is_invite_blob_rejects_missing_type() {
294 let json = br#"{"headCid": "bafyabc"}"#;
295 assert!(!is_invite_blob(json));
296 }
297
298 #[test]
299 fn parse_invite_roundtrip() {
300 let (mut invite, signing_key) = make_test_invite();
301 invite.sign(&signing_key);
302
303 let json = serde_json::to_vec(&invite).unwrap();
304 let parsed = parse_invite(&json).unwrap();
305 assert!(parsed.verify());
306 assert_eq!(parsed.head_cid, invite.head_cid);
307 }
308
309 #[test]
310 fn signable_bytes_deterministic() {
311 let (invite, _) = make_test_invite();
312 let a = invite.signable_bytes();
313 let b = invite.signable_bytes();
314 assert_eq!(a, b);
315 }
316
317 #[test]
318 fn signable_bytes_differ_with_different_data() {
319 let (mut invite_a, _) = make_test_invite();
320 let invite_b = invite_a.clone();
321
322 invite_a.head_cid = "bafydifferent".to_string();
323 assert_ne!(invite_a.signable_bytes(), invite_b.signable_bytes());
324 }
325}