1use core_crypto_keystore::{
2 CryptoKeystoreError, Transaction,
3 entities::ProteusPrekey,
4 traits::{Entity, EntityDatabaseMutation as _, FetchFromDatabase as _},
5};
6use proteus_wasm::keys::PreKeyBundle;
7
8use super::ProteusCentral;
9use crate::{KeystoreError, ProteusError, Result};
10
11impl ProteusCentral {
12 pub(crate) async fn new_prekey(&self, id: u16, transaction: &Transaction) -> Result<Vec<u8>> {
18 use proteus_wasm::keys::{PreKey, PreKeyId};
19
20 if transaction
26 .get::<ProteusPrekey>(&id)
27 .await
28 .map_err(KeystoreError::wrap("checking whether a proteus prekey id is free"))?
29 .is_some()
30 {
31 return Err(
32 KeystoreError::wrap("saving keystore prekey")(CryptoKeystoreError::AlreadyExists(
33 <ProteusPrekey as Entity>::TABLE_NAME,
34 ))
35 .into(),
36 );
37 }
38
39 let prekey_id = PreKeyId::new(id);
40 let prekey = PreKey::new(prekey_id);
41 let keystore_prekey = core_crypto_keystore::entities::ProteusPrekey::from_raw(
42 id,
43 prekey.serialise().map_err(ProteusError::wrap("serialising prekey"))?,
44 );
45 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &prekey);
46 let bundle = bundle
47 .serialise()
48 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
49 keystore_prekey
50 .save(transaction)
51 .map_err(KeystoreError::wrap("saving keystore prekey"))?;
52 Ok(bundle)
53 }
54
55 pub(crate) async fn new_prekey_auto(&self, transaction: &Transaction) -> Result<(u16, Vec<u8>)> {
63 let id = {
66 let conn = transaction
67 .conn()
68 .map_err(KeystoreError::wrap("getting connection from transaction"))?;
69 ProteusPrekey::get_free_id(&conn).map_err(KeystoreError::wrap("getting proteus prekey by id"))?
70 };
71 Ok((id, self.new_prekey(id, transaction).await?))
72 }
73
74 pub fn last_resort_prekey_id() -> u16 {
76 proteus_wasm::keys::MAX_PREKEY_ID.value()
77 }
78
79 pub(crate) async fn last_resort_prekey(&self, transaction: &Transaction) -> Result<Vec<u8>> {
82 let last_resort = if let Some(last_resort) = transaction
83 .get::<core_crypto_keystore::entities::ProteusPrekey>(&Self::last_resort_prekey_id())
84 .await
85 .map_err(KeystoreError::wrap("finding proteus prekey"))?
86 {
87 proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
88 .map_err(ProteusError::wrap("deserialising proteus prekey"))?
89 } else {
90 let last_resort = proteus_wasm::keys::PreKey::last_resort();
91 let prekey = last_resort
92 .serialise()
93 .map_err(ProteusError::wrap("serializing last resort prekey"))?;
94
95 ProteusPrekey::from_raw(Self::last_resort_prekey_id(), prekey)
96 .save(transaction)
97 .map_err(KeystoreError::wrap("storing proteus last resort prekey"))?;
98
99 last_resort
100 };
101
102 let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
103 let bundle = bundle
104 .serialise()
105 .map_err(ProteusError::wrap("serialising prekey bundle"))?;
106
107 Ok(bundle)
108 }
109
110 pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
118 let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
119 Ok(prekey.identity_key.fingerprint())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use core_crypto_keystore::DatabaseKey;
126 use rand::RngExt as _;
127
128 use super::*;
129 use crate::test_utils::{proteus_utils::*, *};
130
131 #[macro_rules_attribute::apply(smol_macros::test)]
132 async fn can_produce_proteus_consumed_prekeys() {
133 #[cfg(not(target_os = "unknown"))]
134 let (path, db_file) = tmp_db_file();
135 #[cfg(target_os = "unknown")]
136 let (path, _) = tmp_db_file();
137
138 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
139
140 let key = DatabaseKey::generate();
141 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
142 let tx = keystore.new_transaction().await.unwrap();
143 let mut alice = ProteusCentral::try_new(&tx).await.unwrap();
144
145 let mut bob = CryptoboxLike::init();
146
147 let alice_prekey_bundle_ser = alice.new_prekey(1, &tx).await.unwrap();
148
149 bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
150 let message = b"Hello world!";
151 let encrypted = bob.encrypt(&session_id, message);
152
153 let (_, decrypted) = alice.session_from_message(&tx, &session_id, &encrypted).await.unwrap();
154
155 assert_eq!(message, decrypted.as_slice());
156
157 let encrypted = alice.encrypt(&tx, &session_id, message).await.unwrap();
158 let decrypted = bob.decrypt(&session_id, &encrypted).await;
159
160 assert_eq!(message, decrypted.as_slice());
161 tx.commit().await.unwrap();
162 #[cfg(not(target_os = "unknown"))]
163 drop(db_file);
164 }
165
166 #[macro_rules_attribute::apply(smol_macros::test)]
172 async fn auto_prekeys_fill_holes() {
173 use core_crypto_keystore::entities::ProteusPrekey;
174 const GAP_AMOUNT: usize = 5;
175 const EXTRA_AMOUNT: u16 = 10;
177 const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
178
179 async fn claim(alice: &ProteusCentral, tx: &Transaction, count: usize) -> Vec<u16> {
181 let mut ids = Vec::with_capacity(count);
182 for _ in 0..count {
183 let (pk_id, pkb) = alice.new_prekey_auto(tx).await.unwrap();
184 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
185 assert_eq!(prekey.prekey_id.value(), pk_id, "the bundle must carry the assigned id");
186 ids.push(pk_id);
187 }
188 ids
189 }
190
191 fn pick_gap_ids(rng: &mut impl rand::Rng, count: usize) -> Vec<u16> {
193 let mut ids = Vec::with_capacity(count);
194 while ids.len() < count {
195 let id = rng.random_range(ID_TEST_RANGE);
196 if !ids.contains(&id) {
197 ids.push(id);
198 }
199 }
200 ids.sort();
201 ids
202 }
203
204 fn ascending(ids: &[u16]) -> Vec<u16> {
205 let mut ids = ids.to_owned();
206 ids.sort();
207 ids
208 }
209
210 #[cfg(not(target_os = "unknown"))]
211 let (path, db_file) = tmp_db_file();
212 #[cfg(target_os = "unknown")]
213 let (path, _) = tmp_db_file();
214
215 let key = DatabaseKey::generate();
216 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
217 let tx = keystore.new_transaction().await.unwrap();
218 let alice = ProteusCentral::try_new(&tx).await.unwrap();
219
220 let claimed = claim(&alice, &tx, ID_TEST_RANGE.count()).await;
222 assert_eq!(claimed, ID_TEST_RANGE.collect::<Vec<_>>());
223
224 let mut rng = rand::rng();
225
226 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
228 for gap_id in &gap_ids {
229 ProteusPrekey::delete(&*tx, gap_id).unwrap();
230 }
231 let claimed = claim(&alice, &tx, GAP_AMOUNT).await;
232 assert_eq!(
233 ascending(&claimed),
234 gap_ids,
235 "every deleted id must be reassigned exactly once"
236 );
237
238 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
241 for gap_id in &gap_ids {
242 ProteusPrekey::delete(&*tx, gap_id).unwrap();
243 }
244 let claimed = claim(&alice, &tx, GAP_AMOUNT + EXTRA_AMOUNT as usize).await;
245 let (filled, extended) = claimed.split_at(GAP_AMOUNT);
246 assert_eq!(
247 ascending(filled),
248 gap_ids,
249 "holes must all be filled before the id space is extended"
250 );
251 let high_water_mark = *ID_TEST_RANGE.end();
252 assert_eq!(
253 extended,
254 (high_water_mark + 1..=high_water_mark + EXTRA_AMOUNT).collect::<Vec<_>>(),
255 "once no holes remain, ids extend the id space in ascending order"
256 );
257
258 tx.commit().await.unwrap();
259 #[cfg(not(target_os = "unknown"))]
260 drop(db_file);
261 }
262
263 #[macro_rules_attribute::apply(smol_macros::test)]
266 async fn last_resort_prekey_does_not_exhaust_id_space() {
267 #[cfg(not(target_os = "unknown"))]
268 let (path, db_file) = tmp_db_file();
269 #[cfg(target_os = "unknown")]
270 let (path, _) = tmp_db_file();
271
272 let key = DatabaseKey::generate();
273 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
274
275 let tx = keystore.new_transaction().await.unwrap();
277 let alice = ProteusCentral::try_new(&tx).await.unwrap();
278 alice.last_resort_prekey(&tx).await.unwrap();
279 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
280 assert_eq!(
281 pk_id, 1,
282 "an uncommitted last resort prekey must not block auto prekeys"
283 );
284 tx.commit().await.unwrap();
285
286 let tx = keystore.new_transaction().await.unwrap();
288 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
289 assert_eq!(pk_id, 2, "a persisted last resort prekey must not block auto prekeys");
290 tx.commit().await.unwrap();
291
292 #[cfg(not(target_os = "unknown"))]
293 drop(db_file);
294 }
295
296 #[macro_rules_attribute::apply(smol_macros::test)]
303 async fn cannot_reuse_a_prekey_id() {
304 #[cfg(not(target_os = "unknown"))]
305 let (path, db_file) = tmp_db_file();
306 #[cfg(target_os = "unknown")]
307 let (path, _) = tmp_db_file();
308
309 let key = DatabaseKey::generate();
310 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
311 let tx = keystore.new_transaction().await.unwrap();
312 let alice = ProteusCentral::try_new(&tx).await.unwrap();
313
314 let bundle = alice.new_prekey(1, &tx).await.unwrap();
315
316 assert!(
319 alice.new_prekey(1, &tx).await.is_err(),
320 "an id claimed earlier in this transaction must not be reassigned"
321 );
322 tx.commit().await.unwrap();
323
324 let tx = keystore.new_transaction().await.unwrap();
326 assert!(
327 alice.new_prekey(1, &tx).await.is_err(),
328 "a persisted id must not be reassigned"
329 );
330
331 let stored = tx.get::<ProteusPrekey>(&1).await.unwrap().unwrap();
333 let stored = proteus_wasm::keys::PreKey::deserialise(&stored.prekey).unwrap();
334 let bundle = PreKeyBundle::deserialise(&bundle).unwrap();
335 assert_eq!(
336 stored.key_pair.public_key, bundle.public_key,
337 "the rejected claims must have left the original prekey in place"
338 );
339
340 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
342 assert_eq!(pk_id, 2, "a rejected claim must not poison the transaction");
343 tx.commit().await.unwrap();
344
345 #[cfg(not(target_os = "unknown"))]
346 drop(db_file);
347 }
348}