1use crate::SyncError;
5use coven_crypto::{
6 custody::{KeyError, PersonEntries, PersonRestoreCode, RestoreCodeChange},
7 MaterialError, MemberIdentity, SecretBytes,
8};
9use coven_database::StoreState;
10use coven_format::codes::{KeychainRestoreCode, RestoreCode};
11use coven_foundation::id_source::StoreId;
12use coven_merge::Slot;
13use coven_storage::{RestoreStorage, StorageConfig};
14
15#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct SavedName {
19 pub name: String,
21 pub named_at: Slot,
23}
24
25impl SavedName {
26 pub fn of(store: &coven_database::StoreIdentity) -> Self {
28 Self { name: store.name.clone(), named_at: store.named_at }
29 }
30}
31
32pub(crate) fn save(
36 person: &PersonEntries,
37 code: &RestoreCode,
38 name: &SavedName,
39) -> Result<(), SyncError> {
40 person.change_restore_code(|saved| {
41 let newer = match saved {
42 Some(PersonRestoreCode::Saved(bytes)) => {
43 KeychainRestoreCode::from_bytes(bytes.as_bytes()).ok()
44 }
45 None | Some(PersonRestoreCode::Ambiguous) => None,
46 }
47 .filter(|saved| saved.code.store == code.store && saved.named_at > name.named_at);
48 let (label, named_at) = match &newer {
49 Some(saved) => (saved.name.as_str(), saved.named_at),
50 None => (name.name.as_str(), name.named_at),
51 };
52 Ok::<_, SyncError>(RestoreCodeChange::Write(encode(person, label, named_at, code)?))
53 })
54}
55
56pub fn follow_name<E: From<KeyError>>(
68 person: &PersonEntries,
69 name: &SavedName,
70 current: impl FnOnce() -> Result<Option<RestoreCode>, E>,
71) -> Result<(), E> {
72 if !person.supported() {
73 return Ok(());
74 }
75 person.change_restore_code(|saved| {
76 let read = match saved {
77 None => return Ok(RestoreCodeChange::Keep),
78 Some(PersonRestoreCode::Saved(bytes)) => {
79 read_saved_code(person.store(), bytes.as_bytes())
80 }
81 Some(PersonRestoreCode::Ambiguous) => Err(UnreadableSavedCode::Ambiguous),
82 };
83 let saved = match read {
84 Ok((saved, _)) => saved,
85 Err(reason) => {
86 let Some(code) = current()? else {
87 tracing::warn!(
88 ?reason,
89 "keeping an unreadable saved restore code: this device holds no current code"
90 );
91 return Ok(RestoreCodeChange::Keep);
92 };
93 tracing::warn!(?reason, "replacing an unreadable saved restore code");
94 let bytes = encode(person, &name.name, name.named_at, &code)?;
95 return Ok(RestoreCodeChange::Write(bytes));
96 }
97 };
98 if saved.named_at >= name.named_at {
99 return Ok(RestoreCodeChange::Keep);
100 }
101 tracing::debug!(
102 from = saved.named_at.get(),
103 to = name.named_at.get(),
104 "rewriting the saved restore code's store name"
105 );
106 let bytes = encode(person, &name.name, name.named_at, &saved.code)?;
107 Ok(RestoreCodeChange::Write(bytes))
108 })
109}
110
111pub fn replacement_code(
116 state: &StoreState,
117 identity: Option<&MemberIdentity>,
118 location: Option<&StorageConfig>,
119) -> Result<Option<RestoreCode>, SyncError> {
120 let (Some(identity), Some(location)) = (identity, location) else {
121 return Ok(None);
122 };
123 let storage = RestoreStorage::location_only(location.clone());
124 match crate::restore_codes::member_code(state, identity, &storage) {
125 Ok(code) => Ok(Some(code)),
126 Err(SyncError::NoStorage | SyncError::Removed | SyncError::MemberKeysReplaced { .. }) => {
127 Ok(None)
128 }
129 Err(error) => Err(error),
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum UnreadableSavedCode {
137 Format,
142 OtherStore,
144 Ambiguous,
147}
148
149pub fn read_saved_code(
151 store: StoreId,
152 bytes: &[u8],
153) -> Result<(KeychainRestoreCode, StorageConfig), UnreadableSavedCode> {
154 let saved = KeychainRestoreCode::from_bytes(bytes).map_err(|error| {
155 tracing::warn!(%store, %error, "saved restore code does not decode");
156 UnreadableSavedCode::Format
157 })?;
158 if saved.code.store != store {
159 tracing::warn!(%store, other = %saved.code.store, "saved restore code names another store");
160 return Err(UnreadableSavedCode::OtherStore);
161 }
162 let storage = RestoreStorage::decode(saved.code.storage.as_bytes()).map_err(|error| {
163 tracing::warn!(%store, %error, "saved restore code's location does not decode");
164 UnreadableSavedCode::Format
165 })?;
166 if storage.s3_key.is_some() {
168 tracing::warn!(%store, "saved restore code carries an S3 key");
169 return Err(UnreadableSavedCode::Format);
170 }
171 Ok((saved, storage.location))
172}
173
174#[expect(
180 clippy::map_err_ignore,
181 reason = "the encoder fails only on a name past its bound or on memory: Encoding"
182)]
183fn encode(
184 person: &PersonEntries,
185 name: &str,
186 named_at: Slot,
187 code: &RestoreCode,
188) -> Result<SecretBytes, KeyError> {
189 let encode = |name: &str| {
190 KeychainRestoreCode::encode(name, named_at, code)
191 .map_err(|_| KeyError::from(MaterialError::Encoding))
192 };
193 let whole = encode(name)?;
194 let Some(limit) = person.restore_code_limit() else {
195 return Ok(SecretBytes::new(whole.to_vec()));
196 };
197 if whole.len() <= limit {
198 return Ok(SecretBytes::new(whole.to_vec()));
199 }
200 let room = limit.checked_sub(whole.len() - name.len()).ok_or(KeyError::SavedEntryTooLarge)?;
201 let shortened = &name[..name.floor_char_boundary(room)];
202 if shortened.is_empty() {
203 return Err(KeyError::SavedEntryTooLarge);
204 }
205 tracing::info!(
206 from = name.len(),
207 to = shortened.len(),
208 "saving the store's name shortened to fit the entry"
209 );
210 Ok(SecretBytes::new(encode(shortened)?.to_vec()))
211}
212
213#[cfg(test)]
214#[path = "saved_code_tests.rs"]
215mod tests;