From 3ff4cbdbd8748597394d3bd6a1631ed03ed6509a Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 22 Jul 2024 00:40:46 +0200 Subject: [PATCH 1/2] Add support for generic public key caching The cache must be provided by the user of this crate by implementing the `PublicKeyCache` trait. --- CHANGELOG.md | 3 +++ examples/lookup_pubkey.rs | 47 +++++++++++++++++++++++++++++++++------ src/api.rs | 35 ++++++++++++++++++++++++++--- src/cache.rs | 22 ++++++++++++++++++ src/errors.rs | 8 +++++++ src/lib.rs | 2 ++ 6 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/cache.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6d7e8cc..ecd6d5c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Possible log types: ### Unreleased +- [added] Add support for generic public key caching:`lookup_pubkey_with_cache` + API method and PublicKeyCache` trait (#77) + ### v0.18.0 (2024-07-13) - [changed] Upgrade `reqwest` dependency from 0.11 to 0.12, and thus indirectly diff --git a/examples/lookup_pubkey.rs b/examples/lookup_pubkey.rs index 545953add..235a64caa 100644 --- a/examples/lookup_pubkey.rs +++ b/examples/lookup_pubkey.rs @@ -1,11 +1,11 @@ use docopt::Docopt; -use threema_gateway::ApiBuilder; +use threema_gateway::{ApiBuilder, PublicKeyCache}; const USAGE: &str = " -Usage: lookup_pubkey [options] +Usage: lookup_pubkey [--with-cache] Options: - -h, --help Show this help + --with-cache Simulate a cache "; #[tokio::main(flavor = "current_thread")] @@ -18,14 +18,47 @@ async fn main() { let our_id = args.get_str(""); let their_id = args.get_str(""); let secret = args.get_str(""); + let simulate_cache = args.get_bool("--with-cache"); // Fetch recipient public key let api = ApiBuilder::new(our_id, secret).into_simple(); - let recipient_key = api.lookup_pubkey(their_id).await; + let pubkey = if simulate_cache { + let cache = SimulatedCache; + api.lookup_pubkey_with_cache(their_id, &cache) + .await + .unwrap_or_else(|e| { + println!("Could not fetch public key: {}", e); + std::process::exit(1); + }) + } else { + api.lookup_pubkey(their_id).await.unwrap_or_else(|e| { + println!("Could not fetch and cache public key: {}", e); + std::process::exit(1); + }) + }; // Show result - match recipient_key { - Ok(key) => println!("Public key for {} is {}.", their_id, key.to_hex_string()), - Err(e) => println!("Could not fetch public key: {}", e), + println!("Public key for {} is {}.", their_id, pubkey.to_hex_string()); +} + +struct SimulatedCache; + +impl PublicKeyCache for SimulatedCache { + type Error = std::io::Error; + + async fn store( + &self, + identity: &str, + _key: &threema_gateway::RecipientKey, + ) -> Result<(), Self::Error> { + println!("[cache] Storing public key for identity {identity}"); + Ok(()) + } + + async fn load( + &self, + _identity: &str, + ) -> Result, Self::Error> { + unimplemented!("Not implemented in this example") } } diff --git a/src/api.rs b/src/api.rs index ca2f2d377..8b86ae7e8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,11 +10,12 @@ use data_encoding::HEXLOWER_PERMISSIVE; use reqwest::Client; use crate::{ + cache::PublicKeyCache, connection::{blob_download, blob_upload, send_e2e, send_simple, Recipient}, crypto::{ encrypt, encrypt_file_msg, encrypt_image_msg, encrypt_raw, EncryptedMessage, RecipientKey, }, - errors::{ApiBuilderError, ApiError, CryptoError}, + errors::{ApiBuilderError, ApiError, ApiOrCacheError, CryptoError}, lookup::{ lookup_capabilities, lookup_credits, lookup_id, lookup_pubkey, Capabilities, LookupCriterion, @@ -42,8 +43,9 @@ macro_rules! impl_common_functionality { /// and therefore you can also look up the key associated with a given ID from /// the server. /// - /// It is strongly recommended that you cache the public keys to avoid querying - /// the API for each message. + /// *Note:* It is strongly recommended that you cache the public keys to avoid + /// querying the API for each message. To simplify this, the + /// `lookup_pubkey_with_cache` method can be used instead. pub async fn lookup_pubkey(&self, id: &str) -> Result { lookup_pubkey( &self.client, @@ -55,6 +57,33 @@ macro_rules! impl_common_functionality { .await } + /// Fetch the recipient public key for the specified Threema ID and store it + /// in the [`PublicKeyCache`]. + /// + /// For the end-to-end encrypted mode, you need the public key of the recipient + /// in order to encrypt a message. While it's best to obtain this directly from + /// the recipient (extract it from the QR code), this may not be convenient, + /// and therefore you can also look up the key associated with a given ID from + /// the server. + pub async fn lookup_pubkey_with_cache( + &self, + id: &str, + public_key_cache: &C, + ) -> Result> + where + C: PublicKeyCache, + { + let pubkey = self + .lookup_pubkey(id) + .await + .map_err(ApiOrCacheError::ApiError)?; + public_key_cache + .store(id, &pubkey) + .await + .map_err(ApiOrCacheError::CacheError)?; + Ok(pubkey) + } + /// Look up a Threema ID in the directory. /// /// An ID can be looked up either by a phone number or an e-mail diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 000000000..3d8ea7685 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,22 @@ +use std::future::Future; + +use crate::crypto::RecipientKey; + +/// A cache for Threema public keys +pub trait PublicKeyCache { + /// Error returned if cache operations fail + type Error: std::error::Error; + + /// Store a public key for `identity` in the cache + fn store( + &self, + identity: &str, + key: &RecipientKey, + ) -> impl Future>; + + /// Retrieve a public key for `identity` from the cache + fn load( + &self, + identity: &str, + ) -> impl Future, Self::Error>>; +} diff --git a/src/errors.rs b/src/errors.rs index e242d7734..4ccf66c66 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -72,6 +72,14 @@ impl From for ApiError { } } +#[derive(Debug, Error)] +pub enum ApiOrCacheError { + #[error("api error: {0}")] + ApiError(ApiError), + #[error("cache error: {0}")] + CacheError(C), +} + /// Crypto related errors. #[derive(Debug, PartialEq, Clone, Error)] pub enum CryptoError { diff --git a/src/lib.rs b/src/lib.rs index bdeac8a02..95fd247b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,7 @@ extern crate log; mod api; +mod cache; mod connection; mod crypto; pub mod errors; @@ -88,6 +89,7 @@ pub use crypto_secretbox::Nonce; pub use crate::{ api::{ApiBuilder, E2eApi, SimpleApi}, + cache::PublicKeyCache, connection::Recipient, crypto::{ decrypt_file_data, encrypt, encrypt_file_data, encrypt_raw, EncryptedFileData, From cde81f0de949da93afb66d2704e1f87c90c90b1b Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 22 Sep 2024 15:36:49 +0200 Subject: [PATCH 2/2] Bump minimal version to 1.75 --- .clippy.toml | 2 +- .github/workflows/ci.yml | 2 +- src/connection.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.clippy.toml b/.clippy.toml index 1645c19f3..130672505 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1 +1 @@ -msrv = "1.70.0" +msrv = "1.75.0" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3f78b42a..49c8e9154 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: name: CI env: - RUST_VERSION: "1.70.0" + RUST_VERSION: "1.75.0" jobs: test: diff --git a/src/connection.rs b/src/connection.rs index 4237dbab8..e8a804d08 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -99,7 +99,7 @@ pub(crate) async fn send_simple( // Send request log::trace!("Sending HTTP request"); let res = client - .post(&format!("{}/send_simple", endpoint)) + .post(format!("{}/send_simple", endpoint)) .form(¶ms) .header("accept", "application/json") .send() @@ -139,7 +139,7 @@ pub(crate) async fn send_e2e( // Send request log::trace!("Sending HTTP request"); let res = client - .post(&format!("{}/send_e2e", endpoint)) + .post(format!("{}/send_e2e", endpoint)) .form(¶ms) .header("accept", "application/json") .send()