Skip to main content

a7n_core/desk/
records.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use a7n_proto::{
4    MachineId, Pin, ProtoError, limits,
5    v1::{DeskMember, DeskRevocation},
6};
7
8const MEMBER_DOMAIN: &[u8] = b"a7n desk member v1\0";
9const REVOCATION_DOMAIN: &[u8] = b"a7n desk revoke v1\0";
10pub(super) const SIGNATURE_LEN: usize = 64;
11
12/// A malformed desk record or a full persisted desk.
13#[derive(Debug, thiserror::Error)]
14pub enum DeskError {
15    #[error(transparent)]
16    Proto(#[from] ProtoError),
17    #[error(
18        "peer name must contain 1..={} Unicode code points",
19        limits::PEER_NAME_MAX
20    )]
21    BadName,
22    #[error("desk signature must contain {SIGNATURE_LEN} bytes")]
23    BadSignature,
24    #[error("desk record capacity exceeded")]
25    Capacity,
26}
27
28fn validate_name(name: &str) -> Result<(), DeskError> {
29    if name.is_empty()
30        || name.chars().take(limits::PEER_NAME_MAX + 1).count() > limits::PEER_NAME_MAX
31    {
32        return Err(DeskError::BadName);
33    }
34    Ok(())
35}
36
37/// Canonical member signing bytes (PROTOCOL.md 6). Name length counts UTF-8 bytes.
38///
39/// # Errors
40/// Returns [`DeskError::BadName`] for a name outside the wire schema's bound.
41pub fn member_signing_input(
42    id: MachineId,
43    pin: Pin,
44    name: &str,
45    introduced_by: MachineId,
46    added_at: u64,
47) -> Result<Vec<u8>, DeskError> {
48    validate_name(name)?;
49    let name_len = u16::try_from(name.len()).map_err(|_| DeskError::BadName)?;
50    let mut input = MEMBER_DOMAIN.to_vec();
51    input.extend_from_slice(id.as_bytes());
52    input.extend_from_slice(pin.as_bytes());
53    input.extend_from_slice(&name_len.to_be_bytes());
54    input.extend_from_slice(name.as_bytes());
55    input.extend_from_slice(introduced_by.as_bytes());
56    input.extend_from_slice(&added_at.to_be_bytes());
57    Ok(input)
58}
59
60/// Canonical revocation signing bytes (PROTOCOL.md 6).
61#[must_use]
62pub fn revocation_signing_input(id: MachineId, revoked_by: MachineId, revoked_at: u64) -> Vec<u8> {
63    let mut input = REVOCATION_DOMAIN.to_vec();
64    input.extend_from_slice(id.as_bytes());
65    input.extend_from_slice(revoked_by.as_bytes());
66    input.extend_from_slice(&revoked_at.to_be_bytes());
67    input
68}
69
70/// A structurally checked membership record; trust and signatures are checked by merge.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Member {
73    pub(super) id: MachineId,
74    pub(super) pin: Pin,
75    pub(super) name: String,
76    pub(super) introduced_by: MachineId,
77    pub(super) added_at: u64,
78    pub(super) signature: [u8; SIGNATURE_LEN],
79}
80
81impl Member {
82    #[must_use]
83    pub fn id(&self) -> MachineId {
84        self.id
85    }
86    #[must_use]
87    pub fn pin(&self) -> Pin {
88        self.pin
89    }
90    #[must_use]
91    pub fn name(&self) -> &str {
92        &self.name
93    }
94    #[must_use]
95    pub fn introduced_by(&self) -> MachineId {
96        self.introduced_by
97    }
98    #[must_use]
99    pub fn added_at(&self) -> u64 {
100        self.added_at
101    }
102    #[must_use]
103    pub fn signature(&self) -> &[u8] {
104        &self.signature
105    }
106
107    pub(super) fn signing_input(&self) -> Vec<u8> {
108        member_signing_input(
109            self.id,
110            self.pin,
111            &self.name,
112            self.introduced_by,
113            self.added_at,
114        )
115        .expect("Member has a checked name")
116    }
117}
118
119impl TryFrom<&DeskMember> for Member {
120    type Error = DeskError;
121
122    fn try_from(record: &DeskMember) -> Result<Self, Self::Error> {
123        validate_name(&record.name)?;
124        Ok(Self {
125            id: MachineId::try_from(record.id.as_slice())?,
126            pin: Pin::try_from(record.pin.as_slice())?,
127            name: record.name.clone(),
128            introduced_by: MachineId::try_from(record.introduced_by.as_slice())?,
129            added_at: record.added_at,
130            signature: record
131                .signature
132                .as_slice()
133                .try_into()
134                .map_err(|_| DeskError::BadSignature)?,
135        })
136    }
137}
138
139impl From<&Member> for DeskMember {
140    fn from(record: &Member) -> Self {
141        Self {
142            id: record.id.as_bytes().to_vec(),
143            pin: record.pin.as_bytes().to_vec(),
144            name: record.name.clone(),
145            introduced_by: record.introduced_by.as_bytes().to_vec(),
146            added_at: record.added_at,
147            signature: record.signature.to_vec(),
148        }
149    }
150}
151
152/// A structurally checked revocation record.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct Revocation {
155    pub(super) id: MachineId,
156    pub(super) revoked_by: MachineId,
157    pub(super) revoked_at: u64,
158    pub(super) signature: [u8; SIGNATURE_LEN],
159}
160
161impl Revocation {
162    #[must_use]
163    pub fn id(&self) -> MachineId {
164        self.id
165    }
166    #[must_use]
167    pub fn revoked_by(&self) -> MachineId {
168        self.revoked_by
169    }
170    #[must_use]
171    pub fn revoked_at(&self) -> u64 {
172        self.revoked_at
173    }
174    #[must_use]
175    pub fn signature(&self) -> &[u8] {
176        &self.signature
177    }
178}
179
180impl TryFrom<&DeskRevocation> for Revocation {
181    type Error = DeskError;
182
183    fn try_from(record: &DeskRevocation) -> Result<Self, Self::Error> {
184        Ok(Self {
185            id: MachineId::try_from(record.id.as_slice())?,
186            revoked_by: MachineId::try_from(record.revoked_by.as_slice())?,
187            revoked_at: record.revoked_at,
188            signature: record
189                .signature
190                .as_slice()
191                .try_into()
192                .map_err(|_| DeskError::BadSignature)?,
193        })
194    }
195}
196
197impl From<&Revocation> for DeskRevocation {
198    fn from(record: &Revocation) -> Self {
199        Self {
200            id: record.id.as_bytes().to_vec(),
201            revoked_by: record.revoked_by.as_bytes().to_vec(),
202            revoked_at: record.revoked_at,
203            signature: record.signature.to_vec(),
204        }
205    }
206}