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
121 use super::*;
122 use crate::test_utils::{proteus_utils::*, *};
123
124 #[macro_rules_attribute::apply(smol_macros::test)]
125 async fn can_produce_proteus_consumed_prekeys() {
126 #[cfg(not(target_os = "unknown"))]
127 let (path, db_file) = tmp_db_file();
128 #[cfg(target_os = "unknown")]
129 let (path, _) = tmp_db_file();
130
131 let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
132
133 let key = DatabaseKey::generate();
134 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
135 let tx = keystore.new_transaction().await.unwrap();
136 let mut alice = ProteusCentral::try_new(&tx).await.unwrap();
137
138 let mut bob = CryptoboxLike::init();
139
140 let alice_prekey_bundle_ser = alice.new_prekey(1, &tx).await.unwrap();
141
142 bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
143 let message = b"Hello world!";
144 let encrypted = bob.encrypt(&session_id, message);
145
146 let (_, decrypted) = alice.session_from_message(&tx, &session_id, &encrypted).await.unwrap();
147
148 assert_eq!(message, decrypted.as_slice());
149
150 let encrypted = alice.encrypt(&tx, &session_id, message).await.unwrap();
151 let decrypted = bob.decrypt(&session_id, &encrypted).await;
152
153 assert_eq!(message, decrypted.as_slice());
154 tx.commit().await.unwrap();
155 #[cfg(not(target_os = "unknown"))]
156 drop(db_file);
157 }
158
159 #[macro_rules_attribute::apply(smol_macros::test)]
165 async fn auto_prekeys_fill_holes() {
166 use core_crypto_keystore::entities::ProteusPrekey;
167 const GAP_AMOUNT: usize = 5;
168 const EXTRA_AMOUNT: u16 = 10;
170 const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
171
172 async fn claim(alice: &ProteusCentral, tx: &Transaction, count: usize) -> Vec<u16> {
174 let mut ids = Vec::with_capacity(count);
175 for _ in 0..count {
176 let (pk_id, pkb) = alice.new_prekey_auto(tx).await.unwrap();
177 let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
178 assert_eq!(prekey.prekey_id.value(), pk_id, "the bundle must carry the assigned id");
179 ids.push(pk_id);
180 }
181 ids
182 }
183
184 fn pick_gap_ids(rng: &mut impl rand::Rng, count: usize) -> Vec<u16> {
186 let mut ids = Vec::with_capacity(count);
187 while ids.len() < count {
188 let id = rng.gen_range(ID_TEST_RANGE);
189 if !ids.contains(&id) {
190 ids.push(id);
191 }
192 }
193 ids.sort();
194 ids
195 }
196
197 fn ascending(ids: &[u16]) -> Vec<u16> {
198 let mut ids = ids.to_owned();
199 ids.sort();
200 ids
201 }
202
203 #[cfg(not(target_os = "unknown"))]
204 let (path, db_file) = tmp_db_file();
205 #[cfg(target_os = "unknown")]
206 let (path, _) = tmp_db_file();
207
208 let key = DatabaseKey::generate();
209 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
210 let tx = keystore.new_transaction().await.unwrap();
211 let alice = ProteusCentral::try_new(&tx).await.unwrap();
212
213 let claimed = claim(&alice, &tx, ID_TEST_RANGE.count()).await;
215 assert_eq!(claimed, ID_TEST_RANGE.collect::<Vec<_>>());
216
217 let mut rng = rand::thread_rng();
218
219 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
221 for gap_id in &gap_ids {
222 ProteusPrekey::delete(&*tx, gap_id).unwrap();
223 }
224 let claimed = claim(&alice, &tx, GAP_AMOUNT).await;
225 assert_eq!(
226 ascending(&claimed),
227 gap_ids,
228 "every deleted id must be reassigned exactly once"
229 );
230
231 let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
234 for gap_id in &gap_ids {
235 ProteusPrekey::delete(&*tx, gap_id).unwrap();
236 }
237 let claimed = claim(&alice, &tx, GAP_AMOUNT + EXTRA_AMOUNT as usize).await;
238 let (filled, extended) = claimed.split_at(GAP_AMOUNT);
239 assert_eq!(
240 ascending(filled),
241 gap_ids,
242 "holes must all be filled before the id space is extended"
243 );
244 let high_water_mark = *ID_TEST_RANGE.end();
245 assert_eq!(
246 extended,
247 (high_water_mark + 1..=high_water_mark + EXTRA_AMOUNT).collect::<Vec<_>>(),
248 "once no holes remain, ids extend the id space in ascending order"
249 );
250
251 tx.commit().await.unwrap();
252 #[cfg(not(target_os = "unknown"))]
253 drop(db_file);
254 }
255
256 #[macro_rules_attribute::apply(smol_macros::test)]
259 async fn last_resort_prekey_does_not_exhaust_id_space() {
260 #[cfg(not(target_os = "unknown"))]
261 let (path, db_file) = tmp_db_file();
262 #[cfg(target_os = "unknown")]
263 let (path, _) = tmp_db_file();
264
265 let key = DatabaseKey::generate();
266 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
267
268 let tx = keystore.new_transaction().await.unwrap();
270 let alice = ProteusCentral::try_new(&tx).await.unwrap();
271 alice.last_resort_prekey(&tx).await.unwrap();
272 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
273 assert_eq!(
274 pk_id, 1,
275 "an uncommitted last resort prekey must not block auto prekeys"
276 );
277 tx.commit().await.unwrap();
278
279 let tx = keystore.new_transaction().await.unwrap();
281 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
282 assert_eq!(pk_id, 2, "a persisted last resort prekey must not block auto prekeys");
283 tx.commit().await.unwrap();
284
285 #[cfg(not(target_os = "unknown"))]
286 drop(db_file);
287 }
288
289 #[macro_rules_attribute::apply(smol_macros::test)]
296 async fn cannot_reuse_a_prekey_id() {
297 #[cfg(not(target_os = "unknown"))]
298 let (path, db_file) = tmp_db_file();
299 #[cfg(target_os = "unknown")]
300 let (path, _) = tmp_db_file();
301
302 let key = DatabaseKey::generate();
303 let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
304 let tx = keystore.new_transaction().await.unwrap();
305 let alice = ProteusCentral::try_new(&tx).await.unwrap();
306
307 let bundle = alice.new_prekey(1, &tx).await.unwrap();
308
309 assert!(
312 alice.new_prekey(1, &tx).await.is_err(),
313 "an id claimed earlier in this transaction must not be reassigned"
314 );
315 tx.commit().await.unwrap();
316
317 let tx = keystore.new_transaction().await.unwrap();
319 assert!(
320 alice.new_prekey(1, &tx).await.is_err(),
321 "a persisted id must not be reassigned"
322 );
323
324 let stored = tx.get::<ProteusPrekey>(&1).await.unwrap().unwrap();
326 let stored = proteus_wasm::keys::PreKey::deserialise(&stored.prekey).unwrap();
327 let bundle = PreKeyBundle::deserialise(&bundle).unwrap();
328 assert_eq!(
329 stored.key_pair.public_key, bundle.public_key,
330 "the rejected claims must have left the original prekey in place"
331 );
332
333 let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
335 assert_eq!(pk_id, 2, "a rejected claim must not poison the transaction");
336 tx.commit().await.unwrap();
337
338 #[cfg(not(target_os = "unknown"))]
339 drop(db_file);
340 }
341}