1use ed25519_dalek::{SigningKey, VerifyingKey};
2use serde::{Deserialize, Serialize};
3
4use super::member::{AuthorizedMember, MemberId};
5use super::member_info::AuthorizedMemberInfo;
6
7const ARMOR_BEGIN: &str = "-----BEGIN RIVER IDENTITY-----";
8const ARMOR_END: &str = "-----END RIVER IDENTITY-----";
9const LINE_WIDTH: usize = 64;
10
11#[derive(Serialize, Deserialize, Clone, Debug)]
14pub struct IdentityExport {
15 pub room_owner: VerifyingKey,
17 pub signing_key: SigningKey,
19 pub authorized_member: AuthorizedMember,
21 pub invite_chain: Vec<AuthorizedMember>,
24 pub member_info: Option<AuthorizedMemberInfo>,
26 #[serde(default)]
28 pub room_name: Option<String>,
29}
30
31impl IdentityExport {
32 pub fn to_armored_string(&self) -> String {
34 let mut data = Vec::new();
35 ciborium::ser::into_writer(self, &mut data).expect("Serialization should not fail");
36 let encoded = bs58::encode(data).into_string();
37
38 let mut result = String::new();
39 result.push_str(ARMOR_BEGIN);
40 result.push('\n');
41 for chunk in encoded.as_bytes().chunks(LINE_WIDTH) {
42 result.push_str(std::str::from_utf8(chunk).unwrap());
43 result.push('\n');
44 }
45 result.push_str(ARMOR_END);
46 result
47 }
48
49 pub fn from_armored_string(s: &str) -> Result<Self, String> {
51 let payload: String = s
53 .lines()
54 .map(|line| line.trim())
55 .filter(|line| !line.is_empty() && !line.starts_with("-----"))
56 .collect();
57
58 if payload.is_empty() {
59 return Err("Empty identity token".to_string());
60 }
61
62 let decoded = bs58::decode(&payload)
63 .into_vec()
64 .map_err(|e| format!("Base58 decode error: {}", e))?;
65 let export: Self = ciborium::de::from_reader(&decoded[..])
66 .map_err(|e| format!("Deserialization error: {}", e))?;
67
68 if export.signing_key.verifying_key() != export.authorized_member.member.member_vk {
70 return Err(
71 "Signing key does not match the authorized member's verifying key".to_string(),
72 );
73 }
74
75 export.validate_invite_chain()?;
79
80 Ok(export)
81 }
82
83 fn validate_invite_chain(&self) -> Result<(), String> {
87 let owner_id = MemberId::from(&self.room_owner);
88
89 let mut vk_by_id: std::collections::HashMap<MemberId, VerifyingKey> =
91 std::collections::HashMap::new();
92 vk_by_id.insert(owner_id, self.room_owner);
93 for chain_member in &self.invite_chain {
94 vk_by_id.insert(chain_member.member.id(), chain_member.member.member_vk);
95 }
96
97 let inviter_id = self.authorized_member.member.invited_by;
99 if let Some(inviter_vk) = vk_by_id.get(&inviter_id) {
100 self.authorized_member
101 .verify_signature(inviter_vk)
102 .map_err(|e| format!("Invalid authorized_member signature: {}", e))?;
103 }
104
105 for chain_member in &self.invite_chain {
107 let inviter_id = chain_member.member.invited_by;
108 if let Some(inviter_vk) = vk_by_id.get(&inviter_id) {
109 chain_member
110 .verify_signature(inviter_vk)
111 .map_err(|e| format!("Invalid invite chain signature: {}", e))?;
112 }
113 }
114
115 Ok(())
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::room_state::member::{Member, MemberId};
123 use crate::room_state::member_info::MemberInfo;
124 use crate::room_state::privacy::SealedBytes;
125 use ed25519_dalek::{Signer, SigningKey};
126 use rand::rngs::OsRng;
127
128 #[test]
129 fn test_roundtrip_armored() {
130 let owner_sk = SigningKey::generate(&mut OsRng);
131 let owner_vk = owner_sk.verifying_key();
132 let owner_id = MemberId::from(&owner_vk);
133
134 let member_sk = SigningKey::generate(&mut OsRng);
135 let member_vk = member_sk.verifying_key();
136
137 let member = Member {
138 owner_member_id: owner_id,
139 invited_by: owner_id,
140 member_vk,
141 };
142 let authorized_member = AuthorizedMember::new(member, &owner_sk);
143
144 let export = IdentityExport {
145 room_owner: owner_vk,
146 signing_key: member_sk,
147 authorized_member,
148 invite_chain: vec![],
149 member_info: None,
150 room_name: None,
151 };
152
153 let armored = export.to_armored_string();
154
155 assert!(armored.starts_with(ARMOR_BEGIN));
157 assert!(armored.trim_end().ends_with(ARMOR_END));
158
159 for line in armored.lines() {
161 if !line.starts_with("-----") {
162 assert!(line.len() <= LINE_WIDTH, "Line too long: {}", line.len());
163 }
164 }
165
166 let decoded = IdentityExport::from_armored_string(&armored).unwrap();
168 assert_eq!(decoded.room_owner.as_bytes(), export.room_owner.as_bytes());
169 assert_eq!(
170 decoded.signing_key.to_bytes(),
171 export.signing_key.to_bytes()
172 );
173 assert_eq!(decoded.authorized_member, export.authorized_member);
174 assert_eq!(decoded.invite_chain.len(), 0);
175 assert!(decoded.member_info.is_none());
176 assert!(decoded.room_name.is_none());
177 }
178
179 #[test]
180 fn test_rejects_mismatched_key() {
181 let owner_sk = SigningKey::generate(&mut OsRng);
182 let owner_vk = owner_sk.verifying_key();
183 let owner_id = MemberId::from(&owner_vk);
184
185 let member_sk = SigningKey::generate(&mut OsRng);
186 let wrong_sk = SigningKey::generate(&mut OsRng);
187 let member_vk = member_sk.verifying_key();
188
189 let member = Member {
190 owner_member_id: owner_id,
191 invited_by: owner_id,
192 member_vk,
193 };
194 let authorized_member = AuthorizedMember::new(member, &owner_sk);
195
196 let export = IdentityExport {
198 room_owner: owner_vk,
199 signing_key: wrong_sk,
200 authorized_member,
201 invite_chain: vec![],
202 member_info: None,
203 room_name: None,
204 };
205
206 let armored = export.to_armored_string();
207 let result = IdentityExport::from_armored_string(&armored);
208 assert!(result.is_err());
209 assert!(result.unwrap_err().contains("does not match"));
210 }
211
212 #[test]
213 fn test_roundtrip_with_invite_chain_and_member_info() {
214 let owner_sk = SigningKey::generate(&mut OsRng);
215 let owner_vk = owner_sk.verifying_key();
216 let owner_id = MemberId::from(&owner_vk);
217
218 let member_a_sk = SigningKey::generate(&mut OsRng);
220 let member_a = Member {
221 owner_member_id: owner_id,
222 invited_by: owner_id,
223 member_vk: member_a_sk.verifying_key(),
224 };
225 let auth_member_a = AuthorizedMember::new(member_a, &owner_sk);
226
227 let member_b_sk = SigningKey::generate(&mut OsRng);
228 let member_b = Member {
229 owner_member_id: owner_id,
230 invited_by: MemberId::from(&member_a_sk.verifying_key()),
231 member_vk: member_b_sk.verifying_key(),
232 };
233 let auth_member_b = AuthorizedMember::new(member_b, &member_a_sk);
234
235 let member_info = MemberInfo {
237 member_id: MemberId::from(&member_b_sk.verifying_key()),
238 version: 1,
239 preferred_nickname: SealedBytes::public("TestUser".as_bytes().to_vec()),
240 };
241 let auth_member_info = AuthorizedMemberInfo::new_with_member_key(member_info, &member_b_sk);
242
243 let export = IdentityExport {
244 room_owner: owner_vk,
245 signing_key: member_b_sk.clone(),
246 authorized_member: auth_member_b.clone(),
247 invite_chain: vec![auth_member_a.clone()],
248 member_info: Some(auth_member_info.clone()),
249 room_name: Some("Test Room".to_string()),
250 };
251
252 let armored = export.to_armored_string();
253 let decoded = IdentityExport::from_armored_string(&armored).unwrap();
254
255 assert_eq!(decoded.invite_chain.len(), 1);
257 assert_eq!(decoded.invite_chain[0], auth_member_a);
258 assert_eq!(decoded.authorized_member, auth_member_b);
259 assert!(decoded.member_info.is_some());
260 assert_eq!(
261 decoded
262 .member_info
263 .unwrap()
264 .member_info
265 .preferred_nickname
266 .to_string_lossy(),
267 "TestUser"
268 );
269 assert_eq!(decoded.room_name.as_deref(), Some("Test Room"));
270 }
271
272 #[test]
273 fn test_imported_key_can_sign() {
274 let owner_sk = SigningKey::generate(&mut OsRng);
275 let owner_vk = owner_sk.verifying_key();
276 let owner_id = MemberId::from(&owner_vk);
277
278 let member_sk = SigningKey::generate(&mut OsRng);
279 let member = Member {
280 owner_member_id: owner_id,
281 invited_by: owner_id,
282 member_vk: member_sk.verifying_key(),
283 };
284 let authorized_member = AuthorizedMember::new(member, &owner_sk);
285
286 let export = IdentityExport {
287 room_owner: owner_vk,
288 signing_key: member_sk,
289 authorized_member,
290 invite_chain: vec![],
291 member_info: None,
292 room_name: None,
293 };
294
295 let armored = export.to_armored_string();
296 let decoded = IdentityExport::from_armored_string(&armored).unwrap();
297
298 let message = b"test message";
300 let signature = decoded.signing_key.sign(message);
301 assert!(decoded
302 .authorized_member
303 .member
304 .member_vk
305 .verify_strict(message, &signature)
306 .is_ok());
307 }
308
309 #[test]
310 fn test_rejects_tampered_signature() {
311 use ed25519_dalek::Signature;
312
313 let owner_sk = SigningKey::generate(&mut OsRng);
314 let owner_vk = owner_sk.verifying_key();
315 let owner_id = MemberId::from(&owner_vk);
316
317 let member_sk = SigningKey::generate(&mut OsRng);
318 let member = Member {
319 owner_member_id: owner_id,
320 invited_by: owner_id,
321 member_vk: member_sk.verifying_key(),
322 };
323 let mut bad_auth_member = AuthorizedMember::new(member, &owner_sk);
325 bad_auth_member.signature = Signature::from_bytes(&[0u8; 64]);
327
328 let export = IdentityExport {
329 room_owner: owner_vk,
330 signing_key: member_sk,
331 authorized_member: bad_auth_member,
332 invite_chain: vec![],
333 member_info: None,
334 room_name: None,
335 };
336
337 let armored = export.to_armored_string();
338 let result = IdentityExport::from_armored_string(&armored);
339 assert!(result.is_err());
340 assert!(result.unwrap_err().contains("signature"));
341 }
342
343 #[test]
344 fn test_rejects_truncated_token() {
345 let owner_sk = SigningKey::generate(&mut OsRng);
346 let owner_vk = owner_sk.verifying_key();
347 let owner_id = MemberId::from(&owner_vk);
348
349 let member_sk = SigningKey::generate(&mut OsRng);
350 let member = Member {
351 owner_member_id: owner_id,
352 invited_by: owner_id,
353 member_vk: member_sk.verifying_key(),
354 };
355 let authorized_member = AuthorizedMember::new(member, &owner_sk);
356
357 let export = IdentityExport {
358 room_owner: owner_vk,
359 signing_key: member_sk,
360 authorized_member,
361 invite_chain: vec![],
362 member_info: None,
363 room_name: None,
364 };
365
366 let armored = export.to_armored_string();
367
368 let lines: Vec<&str> = armored.lines().collect();
370 let truncated = format!(
371 "{}\n{}\n{}",
372 lines[0],
373 &lines[1][..lines[1].len() / 2],
374 lines.last().unwrap()
375 );
376 let result = IdentityExport::from_armored_string(&truncated);
377 assert!(result.is_err());
378 }
379
380 #[test]
381 fn test_rejects_empty_token() {
382 let result = IdentityExport::from_armored_string("");
383 assert!(result.is_err());
384 assert!(result.unwrap_err().contains("Empty"));
385
386 let result = IdentityExport::from_armored_string(
387 "-----BEGIN RIVER IDENTITY-----\n-----END RIVER IDENTITY-----",
388 );
389 assert!(result.is_err());
390 assert!(result.unwrap_err().contains("Empty"));
391 }
392
393 #[test]
394 fn test_handles_whitespace_and_formatting() {
395 let owner_sk = SigningKey::generate(&mut OsRng);
396 let owner_vk = owner_sk.verifying_key();
397 let owner_id = MemberId::from(&owner_vk);
398
399 let member_sk = SigningKey::generate(&mut OsRng);
400 let member = Member {
401 owner_member_id: owner_id,
402 invited_by: owner_id,
403 member_vk: member_sk.verifying_key(),
404 };
405 let authorized_member = AuthorizedMember::new(member, &owner_sk);
406
407 let export = IdentityExport {
408 room_owner: owner_vk,
409 signing_key: member_sk,
410 authorized_member,
411 invite_chain: vec![],
412 member_info: None,
413 room_name: None,
414 };
415
416 let armored = export.to_armored_string();
417
418 let messy = format!("\n {} \n\n", armored.replace('\n', "\n "));
420 let decoded = IdentityExport::from_armored_string(&messy).unwrap();
421 assert_eq!(
422 decoded.signing_key.to_bytes(),
423 export.signing_key.to_bytes()
424 );
425 }
426
427 #[test]
428 fn test_backward_compat_no_room_name() {
429 let owner_sk = SigningKey::generate(&mut OsRng);
432 let owner_vk = owner_sk.verifying_key();
433 let owner_id = MemberId::from(&owner_vk);
434
435 let member_sk = SigningKey::generate(&mut OsRng);
436 let member = Member {
437 owner_member_id: owner_id,
438 invited_by: owner_id,
439 member_vk: member_sk.verifying_key(),
440 };
441 let authorized_member = AuthorizedMember::new(member, &owner_sk);
442
443 #[derive(Serialize)]
445 struct OldExport {
446 room_owner: VerifyingKey,
447 signing_key: SigningKey,
448 authorized_member: AuthorizedMember,
449 invite_chain: Vec<AuthorizedMember>,
450 member_info: Option<AuthorizedMemberInfo>,
451 }
452 let old = OldExport {
453 room_owner: owner_vk,
454 signing_key: member_sk,
455 authorized_member,
456 invite_chain: vec![],
457 member_info: None,
458 };
459 let mut data = Vec::new();
460 ciborium::ser::into_writer(&old, &mut data).unwrap();
461 let encoded = bs58::encode(&data).into_string();
462 let armored = format!("{}\n{}\n{}", ARMOR_BEGIN, encoded, ARMOR_END);
463
464 let decoded = IdentityExport::from_armored_string(&armored).unwrap();
465 assert!(decoded.room_name.is_none());
466 }
467
468 #[test]
469 fn test_owner_self_signed_roundtrip() {
470 let owner_sk = SigningKey::generate(&mut OsRng);
473 let owner_vk = owner_sk.verifying_key();
474 let owner_id = MemberId::from(&owner_vk);
475
476 let member = Member {
478 owner_member_id: owner_id,
479 invited_by: owner_id,
480 member_vk: owner_vk,
481 };
482 let authorized_member = AuthorizedMember::new(member, &owner_sk);
483
484 let export = IdentityExport {
485 room_owner: owner_vk,
486 signing_key: owner_sk,
487 authorized_member,
488 invite_chain: vec![],
489 member_info: None,
490 room_name: Some("My Room".to_string()),
491 };
492
493 let armored = export.to_armored_string();
494 let decoded = IdentityExport::from_armored_string(&armored).unwrap();
495
496 assert_eq!(decoded.room_owner, owner_vk);
497 assert_eq!(decoded.signing_key.verifying_key(), owner_vk);
498 assert_eq!(decoded.authorized_member.member.member_vk, owner_vk);
499 assert!(decoded.invite_chain.is_empty());
500 assert_eq!(decoded.room_name.as_deref(), Some("My Room"));
501
502 let message = b"owner test message";
504 let signature = decoded.signing_key.sign(message);
505 assert!(decoded
506 .authorized_member
507 .member
508 .member_vk
509 .verify_strict(message, &signature)
510 .is_ok());
511 }
512}