Skip to main content

coven_format/
codes.rs

1//! Restore and invite codes keep every owned secret buffer zeroizing (§13).
2
3use crate::code_fields::MAX_STORAGE;
4use crate::error::{bound, require, Error, Rule};
5use crate::kinds;
6use crate::value::name;
7use crate::wire::{decode_frame_of, Decoder, Wire};
8use crate::{frame_prefix, FormatVersion, FRAME_PREFIX_LEN};
9use coven_crypto::{InviteSecret, MemberKeys, SecretBytes};
10use coven_foundation::id_source::{DeviceId, InviteId, KeyId, MemberId, StoreId};
11use coven_merge::Slot;
12use zeroize::Zeroizing;
13
14// Room for the largest invite: two 1,024-byte names, 16 KiB of storage and the
15// frame's fixed fields.
16const MAX_CODE_BYTES: usize = 19 * 1024;
17// A saved code adds its frame prefix, a name of at most 1,024 bytes, its slot
18// and the code's length to the code itself.
19const MAX_KEYCHAIN_BYTES: usize =
20    MAX_CODE_BYTES + crate::FRAME_PREFIX_LEN + 4 + crate::value::MAX_NAME + 8 + 4;
21const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
22
23/// A person's keys and the store's identity and location (§13.1).
24#[derive(Debug)]
25pub struct RestoreCode {
26    /// The store's id.
27    pub store: StoreId,
28    /// The stable member the private pair belongs to. Restore requires this
29    /// id's current pair; an old code cannot rebind it (D13).
30    pub member: MemberId,
31    /// Crypto owns both member key pairs, their encoding and erasure.
32    pub member_keys: MemberKeys,
33    /// Storage location only, interpreted by storage; never credentials.
34    pub storage: SecretBytes,
35}
36impl RestoreCode {
37    /// Encode a bounded frame into a buffer erased on drop, allocated once.
38    pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
39        let keys = self.member_keys.to_secret_bytes();
40        encode_code(
41            kinds::RESTORE_CODE,
42            self.store,
43            &[
44                self.member.0.as_bytes(),
45                &(keys.as_bytes().len() as u32).to_be_bytes(),
46                keys.as_bytes(),
47            ],
48            &self.storage,
49        )
50    }
51    /// Decode a borrowed frame; every owned copy of private material erases on drop.
52    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
53        bound(bytes.len() + 4, MAX_CODE_BYTES, "code bytes")?;
54        let mut input = decode_frame_of(bytes, kinds::RESTORE_CODE, "restore code kind")?;
55        let store = Wire::get(&mut input)?;
56        let member = Wire::get(&mut input)?;
57        let keys_len = u32::get(&mut input)? as usize;
58        let member_keys =
59            MemberKeys::from_secret_bytes(input.take(keys_len)?).map_err(Error::Material)?;
60        let storage = storage(&mut input)?;
61        input.finish()?;
62        Ok(Self { store, member, member_keys, storage })
63    }
64    /// How a restore code's text starts: its kind and version.
65    pub const TEXT_PREFIX: &'static str = "CVR1-";
66    /// Canonical uppercase base32, with CRC-32C, erased on drop.
67    pub fn to_text(&self) -> Result<Zeroizing<String>, Error> {
68        text_encode(Self::TEXT_PREFIX, &self.to_bytes()?)
69    }
70    /// Decode borrowed text without retaining any unerased private copies.
71    pub fn from_text(text: &str) -> Result<Self, Error> {
72        Self::from_bytes(&text_decode(Self::TEXT_PREFIX, text)?)
73    }
74}
75
76/// A restore code as coven saves it in iCloud Keychain or Block Store: the
77/// store's name as the
78/// saving device had it, the slot that set that name, and the code (D13). The
79/// name is a label for the person's list; the code's store id finds the store.
80#[derive(Debug)]
81pub struct KeychainRestoreCode {
82    /// The store's name; a rename made while no device of the person had the
83    /// store open leaves it as they last saw it (§13.1).
84    pub name: String,
85    /// The slot that set `name`: 1 for creation, otherwise the rename's slot.
86    /// Only a name from a later slot replaces it.
87    pub named_at: Slot,
88    /// The member's current restore code.
89    pub code: RestoreCode,
90}
91impl KeychainRestoreCode {
92    /// Encode a bounded frame into a buffer erased on drop, allocated once.
93    pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
94        Self::encode(&self.name, self.named_at, &self.code)
95    }
96    /// Encode the frame for these fields without owning the code.
97    pub fn encode(
98        label: &str,
99        named_at: Slot,
100        code: &RestoreCode,
101    ) -> Result<Zeroizing<Vec<u8>>, Error> {
102        name(label)?;
103        let code = code.to_bytes()?;
104        let size = FRAME_PREFIX_LEN + 4 + label.len() + 8 + 4 + code.len();
105        let mut bytes = Zeroizing::new(Vec::new());
106        bytes.try_reserve_exact(size)?;
107        bytes.extend_from_slice(&frame_prefix(
108            kinds::SAVED_RESTORE_CODE,
109            FormatVersion::CURRENT,
110            (size - FRAME_PREFIX_LEN) as u32,
111        ));
112        bytes.extend_from_slice(&(label.len() as u32).to_be_bytes());
113        bytes.extend_from_slice(label.as_bytes());
114        bytes.extend_from_slice(&named_at.get().to_be_bytes());
115        bytes.extend_from_slice(&(code.len() as u32).to_be_bytes());
116        bytes.extend_from_slice(&code);
117        debug_assert_eq!(bytes.len(), size);
118        Ok(bytes)
119    }
120    /// Decode a borrowed frame; every owned copy of private material erases on drop.
121    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
122        bound(bytes.len(), MAX_KEYCHAIN_BYTES, "saved code bytes")?;
123        let mut input =
124            decode_frame_of(bytes, kinds::SAVED_RESTORE_CODE, "saved restore code kind")?;
125        let label = crate::wire::get_name(&mut input)?;
126        let named_at = <Slot as Wire>::get(&mut input)?;
127        let len = u32::get(&mut input)? as usize;
128        let code = RestoreCode::from_bytes(input.take(len)?)?;
129        input.finish()?;
130        Ok(Self { name: label, named_at, code })
131    }
132}
133
134/// What an invite is for, fixed when the admin makes it (§13.2, §13.3). It
135/// decides which person a join request may name (`objects::Joiner`).
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum InviteKind {
138    /// A new person: the join request carries the name they typed.
139    NewPerson,
140    /// An existing member getting access again: their profile already has a
141    /// name, so the request carries none.
142    Recovery,
143}
144impl Wire for InviteKind {
145    fn put(&self, out: &mut crate::wire::Encoder) -> Result<(), Error> {
146        match self {
147            Self::NewPerson => 0u8,
148            Self::Recovery => 1,
149        }
150        .put(out)
151    }
152    fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
153        match u8::get(input)? {
154            0 => Ok(Self::NewPerson),
155            1 => Ok(Self::Recovery),
156            tag => Err(Error::UnknownTag { field: "invite kind", tag }),
157        }
158    }
159}
160
161/// A one-time invitation; the secret permits a join request, not store access.
162#[derive(Debug)]
163pub struct InviteCode {
164    /// The store's id.
165    pub store: StoreId,
166    /// The store's display name.
167    pub name: String,
168    /// The inviting admin's name, as their app gave it when it made the
169    /// invite: the joining device can't read who is who until it is let in.
170    pub inviter: String,
171    /// Whether the invite adds a new person or recovers a member.
172    pub kind: InviteKind,
173    /// When the invite ends, in seconds since the Unix epoch by the inviting
174    /// device's clock: a joining device sends no first request after it.
175    pub expires_at: u64,
176    /// The invite named by the join request.
177    pub invite: InviteId,
178    /// The member id the admin fixed: fresh for an addition, or an existing
179    /// active id for a re-binding. The signed request must repeat it (D13).
180    pub member: MemberId,
181    /// Crypto owns the one-time secret and its erasure.
182    pub secret: InviteSecret,
183    /// The store key introduced by the creation entry, even after rotations.
184    pub initial_key: KeyId,
185    /// The device whose claimed approval supplies binding evidence.
186    pub inviting_writer: DeviceId,
187    /// The inviting device's last applied slot.
188    pub read_through: u64,
189    /// Storage settings, including any credentials for the recipient.
190    pub storage: SecretBytes,
191}
192impl InviteCode {
193    /// Where the joining device's request goes: under the device that made the
194    /// invite, as this code names it (§13.2).
195    pub fn request_path(&self) -> crate::path::ObjectPath {
196        crate::path::ObjectPath::join_request(self.store, self.inviting_writer, self.invite)
197    }
198
199    /// Encode a bounded frame into a buffer erased on drop, allocated once.
200    pub fn to_bytes(&self) -> Result<Zeroizing<Vec<u8>>, Error> {
201        let secret = self.secret.to_secret_bytes();
202        name(&self.name)?;
203        name(&self.inviter)?;
204        let kind = match self.kind {
205            InviteKind::NewPerson => 0,
206            InviteKind::Recovery => 1,
207        };
208        encode_code(
209            kinds::INVITE_CODE,
210            self.store,
211            &[
212                &(self.name.len() as u32).to_be_bytes(),
213                self.name.as_bytes(),
214                &(self.inviter.len() as u32).to_be_bytes(),
215                self.inviter.as_bytes(),
216                &[kind],
217                &self.expires_at.to_be_bytes(),
218                self.invite.0.as_bytes(),
219                self.member.0.as_bytes(),
220                secret.as_bytes(),
221                self.initial_key.0.as_bytes(),
222                &self.inviting_writer.0.to_be_bytes(),
223                &self.read_through.to_be_bytes(),
224            ],
225            &self.storage,
226        )
227    }
228    /// Decode a borrowed frame; every owned secret buffer erases on drop.
229    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
230        bound(bytes.len() + 4, MAX_CODE_BYTES, "code bytes")?;
231        let mut input = decode_frame_of(bytes, kinds::INVITE_CODE, "invite code kind")?;
232        let store = Wire::get(&mut input)?;
233        let label = crate::wire::get_name(&mut input)?;
234        let inviter = crate::wire::get_name(&mut input)?;
235        let kind = InviteKind::get(&mut input)?;
236        let expires_at = u64::get(&mut input)?;
237        let invite = Wire::get(&mut input)?;
238        let member = Wire::get(&mut input)?;
239        let mut secret = Zeroizing::new([0; 32]);
240        secret.copy_from_slice(input.take(32)?);
241        let secret = InviteSecret::from_bytes(*secret);
242        let initial_key = Wire::get(&mut input)?;
243        let inviting_writer = Wire::get(&mut input)?;
244        let read_through = u64::get(&mut input)?;
245        let storage = storage(&mut input)?;
246        input.finish()?;
247        Ok(Self {
248            store,
249            name: label,
250            inviter,
251            kind,
252            expires_at,
253            invite,
254            member,
255            secret,
256            initial_key,
257            inviting_writer,
258            read_through,
259            storage,
260        })
261    }
262    /// How an invite code's text starts: its kind and version.
263    pub const TEXT_PREFIX: &'static str = "CVI1-";
264    /// Canonical uppercase base32, with CRC-32C, erased on drop.
265    pub fn to_text(&self) -> Result<Zeroizing<String>, Error> {
266        text_encode(Self::TEXT_PREFIX, &self.to_bytes()?)
267    }
268    /// Decode borrowed text without retaining any unerased private copies.
269    pub fn from_text(text: &str) -> Result<Self, Error> {
270        Self::from_bytes(&text_decode(Self::TEXT_PREFIX, text)?)
271    }
272}
273
274fn storage(input: &mut Decoder<'_>) -> Result<SecretBytes, Error> {
275    let len = u32::get(input)? as usize;
276    bound(len, MAX_STORAGE, "storage settings")?;
277    let source = input.take(len)?;
278    let mut bytes = Zeroizing::new(Vec::new());
279    bytes.try_reserve_exact(len)?;
280    bytes.extend_from_slice(source);
281    Ok(SecretBytes::new(std::mem::take(&mut *bytes)))
282}
283
284fn encode_code(
285    kind: u8,
286    store: StoreId,
287    material: &[&[u8]],
288    storage: &SecretBytes,
289) -> Result<Zeroizing<Vec<u8>>, Error> {
290    bound(storage.as_bytes().len(), MAX_STORAGE, "storage settings")?;
291    let size = FRAME_PREFIX_LEN
292        + 16
293        + material.iter().map(|s| s.len()).sum::<usize>()
294        + 4
295        + storage.as_bytes().len();
296    bound(size + 4, MAX_CODE_BYTES, "code bytes")?;
297    let mut bytes = Zeroizing::new(Vec::new());
298    bytes.try_reserve_exact(size)?;
299    bytes.extend_from_slice(&frame_prefix(
300        kind,
301        FormatVersion::CURRENT,
302        (size - FRAME_PREFIX_LEN) as u32,
303    ));
304    bytes.extend_from_slice(store.0.as_bytes());
305    for part in material {
306        bytes.extend_from_slice(part);
307    }
308    bytes.extend_from_slice(&(storage.as_bytes().len() as u32).to_be_bytes());
309    bytes.extend_from_slice(storage.as_bytes());
310    debug_assert_eq!(bytes.len(), size);
311    Ok(bytes)
312}
313
314// CRC-32C: reflected Castagnoli polynomial, init/final XOR all ones. This is
315// accidental-error detection, not authentication; sealing/signing belong to crypto.
316fn checksum(bytes: &[u8]) -> u32 {
317    let mut crc = u32::MAX;
318    for byte in bytes {
319        crc ^= u32::from(*byte);
320        for _ in 0..8 {
321            crc = (crc >> 1) ^ (0x82f6_3b78 & 0u32.wrapping_sub(crc & 1));
322        }
323    }
324    !crc
325}
326
327fn text_encode(prefix: &str, bytes: &[u8]) -> Result<Zeroizing<String>, Error> {
328    bound(bytes.len() + 4, MAX_CODE_BYTES, "code bytes")?;
329    let crc = Zeroizing::new(checksum(bytes).to_be_bytes());
330    let mut text = Zeroizing::new(String::new());
331    text.try_reserve_exact(prefix.len() + ((bytes.len() + 4) * 8).div_ceil(5))?;
332    text.push_str(prefix);
333    let mut pending = Zeroizing::new(0u16);
334    let mut bits = 0;
335    for byte in bytes.iter().chain(crc.iter()) {
336        *pending = (*pending << 8) | u16::from(*byte);
337        bits += 8;
338        while bits >= 5 {
339            bits -= 5;
340            text.push(ALPHABET[((*pending >> bits) & 31) as usize] as char);
341        }
342    }
343    if bits > 0 {
344        text.push(ALPHABET[((*pending << (5 - bits)) & 31) as usize] as char);
345    }
346    Ok(text)
347}
348
349fn text_decode(prefix: &str, text: &str) -> Result<Zeroizing<Vec<u8>>, Error> {
350    bound(text.len(), prefix.len() + (MAX_CODE_BYTES * 8).div_ceil(5), "code text")?;
351    require(text.starts_with(prefix), "code prefix", Rule::Kind)?;
352    let symbols = &text.as_bytes()[prefix.len()..];
353    require(matches!(symbols.len() % 8, 0 | 2 | 4 | 5 | 7), "base32 length", Rule::CodePadding)?;
354    let mut bytes = Zeroizing::new(Vec::new());
355    bytes.try_reserve_exact(symbols.len() * 5 / 8)?;
356    let mut pending = Zeroizing::new(0u16);
357    let mut bits = 0;
358    for (offset, symbol) in symbols.iter().enumerate() {
359        let value = ALPHABET
360            .iter()
361            .position(|c| c == symbol)
362            .ok_or(Error::CodeCharacter { offset: offset + prefix.len() })?;
363        *pending = (*pending << 5) | value as u16;
364        bits += 5;
365        if bits >= 8 {
366            bits -= 8;
367            bytes.push((*pending >> bits) as u8);
368        }
369    }
370    require(*pending & ((1 << bits) - 1) == 0, "base32 padding bits", Rule::CodePadding)?;
371    let payload_len = bytes.len().checked_sub(4).ok_or(Error::Truncated)?;
372    let mut expected = [0; 4];
373    expected.copy_from_slice(&bytes[payload_len..]);
374    if checksum(&bytes[..payload_len]) != u32::from_be_bytes(expected) {
375        return Err(Error::Checksum);
376    }
377    bytes.truncate(payload_len);
378    Ok(bytes)
379}
380
381#[cfg(test)]
382#[path = "codes_tests.rs"]
383mod tests;