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