Skip to main content

paykit_lib/encrypted_link/
recovery_context.rs

1use crate::{validation::validate_uuid_v4, PaykitError, Result};
2
3/// Recovery attempts bound to one Encrypted Link, in the local peer's order.
4///
5/// Swapping peers also swaps these IDs. An absent ID denotes a peer that has
6/// not published a recovery marker. IDs are public metadata, not key material.
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
8pub struct EncryptedLinkRecoveryContext {
9    local_attempt_id: Option<String>,
10    remote_attempt_id: Option<String>,
11}
12
13impl EncryptedLinkRecoveryContext {
14    /// Validate the local and remote UUID-v4 recovery attempt IDs.
15    pub fn new(local_attempt_id: Option<&str>, remote_attempt_id: Option<&str>) -> Result<Self> {
16        let validate =
17            |id: &str| validate_uuid_v4(id.to_owned(), "Encrypted Link recovery attempt ID");
18        Ok(Self {
19            local_attempt_id: local_attempt_id.map(validate).transpose()?,
20            remote_attempt_id: remote_attempt_id.map(validate).transpose()?,
21        })
22    }
23
24    /// Local peer's recovery attempt, if published.
25    pub fn local_attempt_id(&self) -> Option<&str> {
26        self.local_attempt_id.as_deref()
27    }
28
29    /// Remote peer's recovery attempt, if published.
30    pub fn remote_attempt_id(&self) -> Option<&str> {
31        self.remote_attempt_id.as_deref()
32    }
33
34    pub(super) const ENCODED_LEN: usize = 72;
35
36    pub(super) fn append_bytes(&self, bytes: &mut Vec<u8>, local_first: bool) {
37        let ids = if local_first {
38            [self.local_attempt_id(), self.remote_attempt_id()]
39        } else {
40            [self.remote_attempt_id(), self.local_attempt_id()]
41        };
42        for id in ids {
43            bytes.extend_from_slice(id.map(str::as_bytes).unwrap_or(&[0; 36]));
44        }
45    }
46
47    pub(super) fn from_bytes(bytes: &[u8]) -> Result<Self> {
48        let invalid = || PaykitError::InvalidData {
49            context: "invalid Encrypted Link recovery context".into(),
50            source: None,
51        };
52        if bytes.len() != Self::ENCODED_LEN {
53            return Err(invalid());
54        }
55        fn decode(bytes: &[u8]) -> Result<Option<&str>> {
56            if bytes == [0; 36] {
57                Ok(None)
58            } else {
59                std::str::from_utf8(bytes)
60                    .map(Some)
61                    .map_err(|err| PaykitError::InvalidData {
62                        context: "invalid Encrypted Link recovery attempt ID".into(),
63                        source: Some(err.into()),
64                    })
65            }
66        }
67        Self::new(decode(&bytes[..36])?, decode(&bytes[36..])?).map_err(|_| invalid())
68    }
69}