1use std::fmt;
17
18use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
19
20macro_rules! define_hex_key_newtype {
35 (
36 $(#[$meta:meta])*
37 $name:ident
38 ) => {
39 $(#[$meta])*
40 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
41 pub struct $name([u8; 32]);
42
43 impl $name {
44 pub fn from_bytes(bytes: [u8; 32]) -> Self {
46 Self(bytes)
47 }
48
49 pub fn as_bytes(&self) -> &[u8; 32] {
51 &self.0
52 }
53
54 pub fn to_hex(&self) -> String {
56 hex::encode(self.0)
57 }
58
59 pub fn from_hex(s: &str) -> Result<Self, ParseError> {
61 let bytes = hex::decode(s).map_err(|_| ParseError::InvalidHex)?;
62 let arr: [u8; 32] = bytes
63 .try_into()
64 .map_err(|_| ParseError::InvalidLength { expected: 32 })?;
65 Ok(Self(arr))
66 }
67 }
68
69 impl fmt::Debug for $name {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "{}({}…)", stringify!($name), &self.to_hex()[..8])
72 }
73 }
74
75 impl fmt::Display for $name {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(f, "{}", self.to_hex())
78 }
79 }
80
81 impl Serialize for $name {
82 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83 where
84 S: Serializer,
85 {
86 serializer.serialize_str(&self.to_hex())
87 }
88 }
89
90 impl<'de> Deserialize<'de> for $name {
91 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92 where
93 D: Deserializer<'de>,
94 {
95 let s = String::deserialize(deserializer)?;
96 Self::from_hex(&s).map_err(de::Error::custom)
97 }
98 }
99 };
100}
101
102define_hex_key_newtype!(
107 NostrPubKey
111);
112
113define_hex_key_newtype!(
118 SigningPubKey
122);
123
124define_hex_key_newtype!(
129 RecipientPubKey
133);
134
135#[derive(Clone, Copy, PartialEq, Eq)]
143pub struct CommitSignature([u8; 64]);
144
145impl CommitSignature {
146 pub fn from_bytes(bytes: [u8; 64]) -> Self {
148 Self(bytes)
149 }
150
151 pub fn as_bytes(&self) -> &[u8; 64] {
153 &self.0
154 }
155
156 pub fn to_hex(&self) -> String {
158 hex::encode(self.0)
159 }
160}
161
162impl fmt::Debug for CommitSignature {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "CommitSignature({}...)", &hex::encode(&self.0[..8]))
165 }
166}
167
168impl Serialize for CommitSignature {
169 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170 where
171 S: Serializer,
172 {
173 serializer.serialize_str(&self.to_hex())
174 }
175}
176
177impl<'de> Deserialize<'de> for CommitSignature {
178 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179 where
180 D: Deserializer<'de>,
181 {
182 let s = String::deserialize(deserializer)?;
183 let bytes = hex::decode(&s).map_err(de::Error::custom)?;
184 let arr: [u8; 64] = bytes
185 .try_into()
186 .map_err(|_| de::Error::custom("signature must be 64 bytes"))?;
187 Ok(Self(arr))
188 }
189}
190
191#[derive(Clone, PartialEq, Eq)]
199pub struct WrappedKey(Vec<u8>);
200
201impl WrappedKey {
202 pub fn from_bytes(bytes: Vec<u8>) -> Self {
204 Self(bytes)
205 }
206
207 pub fn as_bytes(&self) -> &[u8] {
209 &self.0
210 }
211
212 pub fn into_bytes(self) -> Vec<u8> {
214 self.0
215 }
216
217 pub fn to_base64(&self) -> String {
219 use base64::{engine::general_purpose::STANDARD, Engine};
220 STANDARD.encode(&self.0)
221 }
222
223 pub fn from_base64(s: &str) -> Result<Self, ParseError> {
225 use base64::{engine::general_purpose::STANDARD, Engine};
226 let bytes = STANDARD.decode(s).map_err(|_| ParseError::InvalidBase64)?;
227 Ok(Self(bytes))
228 }
229}
230
231impl fmt::Debug for WrappedKey {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 write!(f, "WrappedKey({} bytes)", self.0.len())
234 }
235}
236
237impl Serialize for WrappedKey {
238 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
239 where
240 S: Serializer,
241 {
242 serializer.serialize_str(&self.to_base64())
243 }
244}
245
246impl<'de> Deserialize<'de> for WrappedKey {
247 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
248 where
249 D: Deserializer<'de>,
250 {
251 let s = String::deserialize(deserializer)?;
252 Self::from_base64(&s).map_err(de::Error::custom)
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
264pub struct ContributorId {
265 pub signing: SigningPubKey,
267 pub recipient: RecipientPubKey,
269}
270
271impl ContributorId {
272 pub fn new(signing: SigningPubKey, recipient: RecipientPubKey) -> Self {
274 Self { signing, recipient }
275 }
276
277 pub fn from_uri(s: &str) -> Result<Self, ParseError> {
279 let s = s
280 .strip_prefix("void://")
281 .ok_or(ParseError::InvalidFormat("missing void:// prefix"))?;
282
283 let parts: Vec<&str> = s.split('/').collect();
284 if parts.len() != 2 {
285 return Err(ParseError::InvalidFormat(
286 "expected format: ed25519:<hex>/x25519:<hex>",
287 ));
288 }
289
290 let signing_hex = parts[0]
291 .strip_prefix("ed25519:")
292 .ok_or(ParseError::InvalidFormat("missing ed25519: prefix"))?;
293 let recipient_hex = parts[1]
294 .strip_prefix("x25519:")
295 .ok_or(ParseError::InvalidFormat("missing x25519: prefix"))?;
296
297 Ok(Self {
298 signing: SigningPubKey::from_hex(signing_hex)?,
299 recipient: RecipientPubKey::from_hex(recipient_hex)?,
300 })
301 }
302
303 pub fn to_uri(&self) -> String {
305 format!(
306 "void://ed25519:{}/x25519:{}",
307 self.signing.to_hex(),
308 self.recipient.to_hex()
309 )
310 }
311}
312
313impl fmt::Display for ContributorId {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(f, "{}", self.to_uri())
316 }
317}
318
319#[derive(Clone, PartialEq, Eq)]
332pub struct RepoKey([u8; 32]);
333
334impl RepoKey {
335 pub fn from_bytes(bytes: [u8; 32]) -> Self {
337 Self(bytes)
338 }
339
340 pub fn as_bytes(&self) -> &[u8; 32] {
342 &self.0
343 }
344}
345
346impl fmt::Debug for RepoKey {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "RepoKey([REDACTED])")
349 }
350}
351
352impl Drop for RepoKey {
353 fn drop(&mut self) {
354 self.0.iter_mut().for_each(|b| *b = 0);
356 }
357}
358
359#[derive(Debug, Clone, thiserror::Error)]
365pub enum ParseError {
366 #[error("invalid hex encoding")]
367 InvalidHex,
368
369 #[error("invalid base64 encoding")]
370 InvalidBase64,
371
372 #[error("invalid length: expected {expected} bytes")]
373 InvalidLength { expected: usize },
374
375 #[error("invalid format: {0}")]
376 InvalidFormat(&'static str),
377}
378
379#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn signing_pubkey_hex_roundtrip() {
389 let bytes = [0x42u8; 32];
390 let key = SigningPubKey::from_bytes(bytes);
391
392 let hex = key.to_hex();
393 let parsed = SigningPubKey::from_hex(&hex).unwrap();
394
395 assert_eq!(key, parsed);
396 assert_eq!(key.as_bytes(), &bytes);
397 }
398
399 #[test]
400 fn signing_pubkey_serde_roundtrip() {
401 let key = SigningPubKey::from_bytes([0xaa; 32]);
402
403 let json = serde_json::to_string(&key).unwrap();
404 assert!(json.contains(&"a".repeat(64)));
405
406 let parsed: SigningPubKey = serde_json::from_str(&json).unwrap();
407 assert_eq!(key, parsed);
408 }
409
410 #[test]
411 fn signing_pubkey_invalid_hex() {
412 assert!(SigningPubKey::from_hex("not-hex").is_err());
413 assert!(SigningPubKey::from_hex("aabb").is_err()); }
415
416 #[test]
417 fn recipient_pubkey_hex_roundtrip() {
418 let bytes = [0x99u8; 32];
419 let key = RecipientPubKey::from_bytes(bytes);
420
421 let hex = key.to_hex();
422 let parsed = RecipientPubKey::from_hex(&hex).unwrap();
423
424 assert_eq!(key, parsed);
425 }
426
427 #[test]
428 fn wrapped_key_base64_roundtrip() {
429 let bytes = vec![1, 2, 3, 4, 5, 6, 7, 8];
430 let key = WrappedKey::from_bytes(bytes.clone());
431
432 let b64 = key.to_base64();
433 let parsed = WrappedKey::from_base64(&b64).unwrap();
434
435 assert_eq!(parsed.as_bytes(), &bytes);
436 }
437
438 #[test]
439 fn wrapped_key_serde_roundtrip() {
440 let key = WrappedKey::from_bytes(vec![0xde, 0xad, 0xbe, 0xef]);
441
442 let json = serde_json::to_string(&key).unwrap();
443 let parsed: WrappedKey = serde_json::from_str(&json).unwrap();
444
445 assert_eq!(key, parsed);
446 }
447
448 #[test]
449 fn contributor_id_uri_roundtrip() {
450 let signing = SigningPubKey::from_bytes([0xaa; 32]);
451 let recipient = RecipientPubKey::from_bytes([0xbb; 32]);
452 let id = ContributorId::new(signing, recipient);
453
454 let uri = id.to_uri();
455 assert!(uri.starts_with("void://ed25519:"));
456 assert!(uri.contains("/x25519:"));
457
458 let parsed = ContributorId::from_uri(&uri).unwrap();
459 assert_eq!(id, parsed);
460 }
461
462 #[test]
463 fn contributor_id_parse_errors() {
464 assert!(ContributorId::from_uri("ed25519:aa/x25519:bb").is_err());
465 assert!(ContributorId::from_uri("void://ed25519:aa").is_err());
466 assert!(ContributorId::from_uri(&format!(
467 "void://{}/x25519:{}",
468 "a".repeat(64),
469 "b".repeat(64)
470 ))
471 .is_err());
472 }
473
474 #[test]
475 fn contributor_id_serde_roundtrip() {
476 let id = ContributorId::new(
477 SigningPubKey::from_bytes([0x11; 32]),
478 RecipientPubKey::from_bytes([0x22; 32]),
479 );
480
481 let json = serde_json::to_string(&id).unwrap();
482 let parsed: ContributorId = serde_json::from_str(&json).unwrap();
483
484 assert_eq!(id, parsed);
485 }
486
487 #[test]
488 fn debug_formats_are_concise() {
489 let signing = SigningPubKey::from_bytes([0xab; 32]);
490 let debug = format!("{:?}", signing);
491 assert!(debug.contains("abababab"));
492 assert!(debug.len() < 50);
493
494 let wrapped = WrappedKey::from_bytes(vec![0; 100]);
495 let debug = format!("{:?}", wrapped);
496 assert!(debug.contains("100 bytes"));
497 }
498
499 #[test]
500 fn repo_key_creation_and_access() {
501 let bytes = [0x42u8; 32];
502 let repo_key = RepoKey::from_bytes(bytes);
503 assert_eq!(repo_key.as_bytes(), &bytes);
504 }
505
506 #[test]
507 fn repo_key_debug_is_redacted() {
508 let repo_key = RepoKey::from_bytes([0x42u8; 32]);
509 let debug = format!("{:?}", repo_key);
510 assert!(!debug.contains("42"));
511 assert!(debug.contains("REDACTED"));
512 }
513
514 #[test]
515 fn repo_key_equality() {
516 let key1 = RepoKey::from_bytes([0x42u8; 32]);
517 let key2 = RepoKey::from_bytes([0x42u8; 32]);
518 let key3 = RepoKey::from_bytes([0x43u8; 32]);
519
520 assert_eq!(key1, key2);
521 assert_ne!(key1, key3);
522 }
523}