Skip to main content

core_crypto/proteus/
prekey.rs

1use core_crypto_keystore::{
2    CryptoKeystoreError, Transaction,
3    entities::ProteusPrekey,
4    traits::{Entity, FetchFromDatabase as _},
5};
6use proteus_wasm::keys::PreKeyBundle;
7
8use super::ProteusCentral;
9use crate::{KeystoreError, ProteusError, Result};
10
11impl ProteusCentral {
12    /// Generates a new Proteus PreKey, stores it in the keystore and returns a serialized PreKeyBundle to be consumed
13    /// externally
14    ///
15    /// Fails if `id` is already taken. Prekeys are not replaceable: the id has been published to
16    /// peers in a bundle, and overwriting it would strand anyone still holding that bundle.
17    pub(crate) async fn new_prekey(&self, id: u16, transaction: &Transaction) -> Result<Vec<u8>> {
18        use proteus_wasm::keys::{PreKey, PreKeyId};
19
20        // The keystore also refuses a duplicate id, but only once the transaction is applied,
21        // which would take down whatever else that transaction was doing and report the conflict
22        // far from the call responsible for it. Catch it here, while we can still name the id and
23        // leave the transaction usable. Reading through the transaction consults its buffered
24        // operations as well as the database, so an id claimed earlier in this same transaction
25        // counts as taken.
26        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        transaction
51            .save(keystore_prekey)
52            .await
53            .map_err(KeystoreError::wrap("saving keystore prekey"))?;
54        Ok(bundle)
55    }
56
57    /// Generates a new Proteus Prekey, with an automatically auto-incremented ID.
58    ///
59    /// See [ProteusCentral::new_prekey]
60    pub(crate) async fn new_prekey_auto(&self, transaction: &Transaction) -> Result<(u16, Vec<u8>)> {
61        let id = core_crypto_keystore::entities::ProteusPrekey::get_free_id(transaction)
62            .await
63            .map_err(KeystoreError::wrap("getting proteus prekey by id"))?;
64        Ok((id, self.new_prekey(id, transaction).await?))
65    }
66
67    /// Returns the Proteus last resort prekey ID (u16::MAX = 65535 = 0xFFFF)
68    pub fn last_resort_prekey_id() -> u16 {
69        proteus_wasm::keys::MAX_PREKEY_ID.value()
70    }
71
72    /// Returns the Proteus last resort prekey
73    /// If it cannot be found, one will be created.
74    pub(crate) async fn last_resort_prekey(&self, transaction: &Transaction) -> Result<Vec<u8>> {
75        let last_resort = if let Some(last_resort) = transaction
76            .get::<core_crypto_keystore::entities::ProteusPrekey>(&Self::last_resort_prekey_id())
77            .await
78            .map_err(KeystoreError::wrap("finding proteus prekey"))?
79        {
80            proteus_wasm::keys::PreKey::deserialise(&last_resort.prekey)
81                .map_err(ProteusError::wrap("deserialising proteus prekey"))?
82        } else {
83            let last_resort = proteus_wasm::keys::PreKey::last_resort();
84            let prekey = last_resort
85                .serialise()
86                .map_err(ProteusError::wrap("serializing last resort prekey"))?;
87
88            transaction
89                .save(ProteusPrekey::from_raw(Self::last_resort_prekey_id(), prekey))
90                .await
91                .map_err(KeystoreError::wrap("storing proteus last resort prekey"))?;
92
93            last_resort
94        };
95
96        let bundle = PreKeyBundle::new(self.proteus_identity.as_ref().public_key.clone(), &last_resort);
97        let bundle = bundle
98            .serialise()
99            .map_err(ProteusError::wrap("serialising prekey bundle"))?;
100
101        Ok(bundle)
102    }
103
104    /// Hex-encoded fingerprint of the given prekey
105    ///
106    /// # Errors
107    /// If the prekey cannot be deserialized
108    pub fn fingerprint_prekeybundle(prekey: &[u8]) -> Result<String> {
109        let prekey = PreKeyBundle::deserialise(prekey).map_err(ProteusError::wrap("deserialising prekey bundle"))?;
110        Ok(prekey.identity_key.fingerprint())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use core_crypto_keystore::DatabaseKey;
117
118    use super::*;
119    use crate::test_utils::{proteus_utils::*, *};
120
121    #[macro_rules_attribute::apply(smol_macros::test)]
122    async fn can_produce_proteus_consumed_prekeys() {
123        #[cfg(not(target_os = "unknown"))]
124        let (path, db_file) = tmp_db_file();
125        #[cfg(target_os = "unknown")]
126        let (path, _) = tmp_db_file();
127
128        let session_id = uuid::Uuid::new_v4().hyphenated().to_string();
129
130        let key = DatabaseKey::generate();
131        let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
132        let tx = keystore.new_transaction().await.unwrap();
133        let mut alice = ProteusCentral::try_new(&tx).await.unwrap();
134
135        let mut bob = CryptoboxLike::init();
136
137        let alice_prekey_bundle_ser = alice.new_prekey(1, &tx).await.unwrap();
138
139        bob.init_session_from_prekey_bundle(&session_id, &alice_prekey_bundle_ser);
140        let message = b"Hello world!";
141        let encrypted = bob.encrypt(&session_id, message);
142
143        let (_, decrypted) = alice.session_from_message(&tx, &session_id, &encrypted).await.unwrap();
144
145        assert_eq!(message, decrypted.as_slice());
146
147        let encrypted = alice.encrypt(&tx, &session_id, message).await.unwrap();
148        let decrypted = bob.decrypt(&session_id, &encrypted).await;
149
150        assert_eq!(message, decrypted.as_slice());
151        tx.commit().await.unwrap();
152        #[cfg(not(target_os = "unknown"))]
153        drop(db_file);
154    }
155
156    /// Auto prekeys fill the holes left by deleted prekeys before extending the id space.
157    ///
158    /// The order in which the holes get filled is deliberately not asserted: the keystore tracks
159    /// ids freed within a transaction in an unordered map, so all that is promised is that every
160    /// hole is filled exactly once before any fresh id is handed out.
161    #[macro_rules_attribute::apply(smol_macros::test)]
162    async fn auto_prekeys_fill_holes() {
163        use core_crypto_keystore::entities::ProteusPrekey;
164        const GAP_AMOUNT: usize = 5;
165        /// How far past the end of the filled id space to keep claiming ids
166        const EXTRA_AMOUNT: u16 = 10;
167        const ID_TEST_RANGE: std::ops::RangeInclusive<u16> = 1..=30;
168
169        /// Claim `count` auto prekeys, returning the assigned ids in the order they were handed out.
170        async fn claim(alice: &ProteusCentral, tx: &Transaction, count: usize) -> Vec<u16> {
171            let mut ids = Vec::with_capacity(count);
172            for _ in 0..count {
173                let (pk_id, pkb) = alice.new_prekey_auto(tx).await.unwrap();
174                let prekey = proteus_wasm::keys::PreKeyBundle::deserialise(&pkb).unwrap();
175                assert_eq!(prekey.prekey_id.value(), pk_id, "the bundle must carry the assigned id");
176                ids.push(pk_id);
177            }
178            ids
179        }
180
181        /// Pick `count` distinct ids from `ID_TEST_RANGE`, in ascending order.
182        fn pick_gap_ids(rng: &mut impl rand::Rng, count: usize) -> Vec<u16> {
183            let mut ids = Vec::with_capacity(count);
184            while ids.len() < count {
185                let id = rng.gen_range(ID_TEST_RANGE);
186                if !ids.contains(&id) {
187                    ids.push(id);
188                }
189            }
190            ids.sort();
191            ids
192        }
193
194        fn ascending(ids: &[u16]) -> Vec<u16> {
195            let mut ids = ids.to_owned();
196            ids.sort();
197            ids
198        }
199
200        #[cfg(not(target_os = "unknown"))]
201        let (path, db_file) = tmp_db_file();
202        #[cfg(target_os = "unknown")]
203        let (path, _) = tmp_db_file();
204
205        let key = DatabaseKey::generate();
206        let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
207        let tx = keystore.new_transaction().await.unwrap();
208        let alice = ProteusCentral::try_new(&tx).await.unwrap();
209
210        // with no holes to fill, ids are simply assigned in ascending order from 1
211        let claimed = claim(&alice, &tx, ID_TEST_RANGE.count()).await;
212        assert_eq!(claimed, ID_TEST_RANGE.collect::<Vec<_>>());
213
214        let mut rng = rand::thread_rng();
215
216        // punch some holes; the next claims must fill exactly those
217        let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
218        for gap_id in &gap_ids {
219            tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
220        }
221        let claimed = claim(&alice, &tx, GAP_AMOUNT).await;
222        assert_eq!(
223            ascending(&claimed),
224            gap_ids,
225            "every deleted id must be reassigned exactly once"
226        );
227
228        // punch holes again, but this time keep claiming past them, to check both that holes take
229        // priority over fresh ids and that the fresh ids resume above the filled id space
230        let gap_ids = pick_gap_ids(&mut rng, GAP_AMOUNT);
231        for gap_id in &gap_ids {
232            tx.remove::<ProteusPrekey>(gap_id).await.unwrap();
233        }
234        let claimed = claim(&alice, &tx, GAP_AMOUNT + EXTRA_AMOUNT as usize).await;
235        let (filled, extended) = claimed.split_at(GAP_AMOUNT);
236        assert_eq!(
237            ascending(filled),
238            gap_ids,
239            "holes must all be filled before the id space is extended"
240        );
241        let high_water_mark = *ID_TEST_RANGE.end();
242        assert_eq!(
243            extended,
244            (high_water_mark + 1..=high_water_mark + EXTRA_AMOUNT).collect::<Vec<_>>(),
245            "once no holes remain, ids extend the id space in ascending order"
246        );
247
248        tx.commit().await.unwrap();
249        #[cfg(not(target_os = "unknown"))]
250        drop(db_file);
251    }
252
253    /// The last resort prekey is stored in the same table as ordinary prekeys, at `u16::MAX`.
254    /// Free-id selection must not treat it as though the id space were exhausted.
255    #[macro_rules_attribute::apply(smol_macros::test)]
256    async fn last_resort_prekey_does_not_exhaust_id_space() {
257        #[cfg(not(target_os = "unknown"))]
258        let (path, db_file) = tmp_db_file();
259        #[cfg(target_os = "unknown")]
260        let (path, _) = tmp_db_file();
261
262        let key = DatabaseKey::generate();
263        let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
264
265        // in this transaction the last resort prekey is only cached, never persisted
266        let tx = keystore.new_transaction().await.unwrap();
267        let alice = ProteusCentral::try_new(&tx).await.unwrap();
268        alice.last_resort_prekey(&tx).await.unwrap();
269        let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
270        assert_eq!(
271            pk_id, 1,
272            "an uncommitted last resort prekey must not block auto prekeys"
273        );
274        tx.commit().await.unwrap();
275
276        // now the last resort prekey is persisted, so it's the database query which has to skip it
277        let tx = keystore.new_transaction().await.unwrap();
278        let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
279        assert_eq!(pk_id, 2, "a persisted last resort prekey must not block auto prekeys");
280        tx.commit().await.unwrap();
281
282        #[cfg(not(target_os = "unknown"))]
283        drop(db_file);
284    }
285
286    /// Claiming a prekey id which is already taken fails, and fails at the call responsible.
287    ///
288    /// The keystore refuses the duplicate on its own, but only when the transaction is applied.
289    /// This exercises the guard in front of it: the caller learns immediately, the prekey already
290    /// stored under that id is left alone, and the transaction survives to commit whatever else
291    /// it was carrying.
292    #[macro_rules_attribute::apply(smol_macros::test)]
293    async fn cannot_reuse_a_prekey_id() {
294        #[cfg(not(target_os = "unknown"))]
295        let (path, db_file) = tmp_db_file();
296        #[cfg(target_os = "unknown")]
297        let (path, _) = tmp_db_file();
298
299        let key = DatabaseKey::generate();
300        let keystore = core_crypto_keystore::Database::open(&path, &key).await.unwrap();
301        let tx = keystore.new_transaction().await.unwrap();
302        let alice = ProteusCentral::try_new(&tx).await.unwrap();
303
304        let bundle = alice.new_prekey(1, &tx).await.unwrap();
305
306        // taken by an id claimed earlier in this same transaction, which is only visible in the
307        // transaction's buffered operations and not yet in the database
308        assert!(
309            alice.new_prekey(1, &tx).await.is_err(),
310            "an id claimed earlier in this transaction must not be reassigned"
311        );
312        tx.commit().await.unwrap();
313
314        // and taken by an id claimed in an earlier transaction, which reaches the database
315        let tx = keystore.new_transaction().await.unwrap();
316        assert!(
317            alice.new_prekey(1, &tx).await.is_err(),
318            "a persisted id must not be reassigned"
319        );
320
321        // the original prekey survived both rejected claims
322        let stored = tx.get::<ProteusPrekey>(&1).await.unwrap().unwrap();
323        let stored = proteus_wasm::keys::PreKey::deserialise(&stored.prekey).unwrap();
324        let bundle = PreKeyBundle::deserialise(&bundle).unwrap();
325        assert_eq!(
326            stored.key_pair.public_key, bundle.public_key,
327            "the rejected claims must have left the original prekey in place"
328        );
329
330        // and the transaction is still usable
331        let (pk_id, _) = alice.new_prekey_auto(&tx).await.unwrap();
332        assert_eq!(pk_id, 2, "a rejected claim must not poison the transaction");
333        tx.commit().await.unwrap();
334
335        #[cfg(not(target_os = "unknown"))]
336        drop(db_file);
337    }
338}