ncryptf/rocket/
ek.rs

1use crate::{Keypair, Signature};
2use base64::{engine::general_purpose, Engine as _};
3use rand::{distr::Alphanumeric, Rng};
4use serde::{Deserialize, Serialize};
5
6/// Reusable encryption key data for client parsing
7///
8/// This is exported for use in your application for deserializing the request.
9#[derive(Serialize, Deserialize, Clone, Debug)]
10pub struct ExportableEncryptionKeyData {
11    pub public: String,
12    pub signature: String,
13    pub hash_id: String,
14    pub expires_at: i64,
15    pub ephemeral: bool,
16}
17
18impl ExportableEncryptionKeyData {
19    /// Returns true if this key is expired
20    pub fn is_expired(&self) -> bool {
21        return chrono::Utc::now().timestamp() >= self.expires_at;
22    }
23
24    /// Returns the public key as a Vec
25    pub fn get_public_key(&self) -> Option<Vec<u8>> {
26        if self.public.is_empty() {
27            return None;
28        }
29
30        return Some(general_purpose::STANDARD.decode(self.public.clone()).unwrap());
31    }
32
33    /// Returns the signature key as a Vec
34    pub fn get_signature_key(&self) -> Option<Vec<u8>> {
35        if self.public.is_empty() {
36            return None;
37        }
38
39        return Some(general_purpose::STANDARD.decode(self.signature.clone()).unwrap());
40    }
41}
42
43/// Represents an Encryption key used to encrypt and decrypt requests
44#[derive(Serialize, Deserialize, Debug, Clone)]
45pub struct EncryptionKey {
46    bkp: Keypair,
47    skp: Keypair,
48    ephemeral: bool,
49    pub expires_at: i64,
50    hash_id: String,
51}
52
53impl EncryptionKey {
54    /// Returns the box keypair
55    pub fn get_box_kp(&self) -> Keypair {
56        return self.bkp.clone();
57    }
58
59    /// Returns the signing keypair
60    pub fn get_sign_kp(&self) -> Keypair {
61        return self.skp.clone();
62    }
63
64    /// Returns true if the key is meant to be used only once
65    pub fn is_ephemeral(&self) -> bool {
66        return self.ephemeral;
67    }
68
69    /// Returns the hash id
70    pub fn get_hash_id(&self) -> String {
71        return self.hash_id.clone();
72    }
73
74    /// Returns true if the token is expired
75    /// Expiration should be handled server side
76    /// But the client should know if they need a new key
77    pub fn is_expired(&self) -> bool {
78        if chrono::Utc::now().timestamp() >= self.expires_at {
79            return true;
80        }
81
82        return false;
83    }
84
85    /// Creates a new struct with an ephemeral flag set
86    pub fn new(ephemeral: bool) -> Self {
87        let s: String = rand::rng()
88            .sample_iter(&Alphanumeric)
89            .take(24)
90            .map(char::from)
91            .collect();
92
93        // Encryption keys are valid for an hour
94        let expiration = chrono::Utc::now() + chrono::Duration::hours(1);
95        return Self {
96            bkp: Keypair::new(),
97            skp: Signature::new(),
98            ephemeral: ephemeral,
99            expires_at: expiration.timestamp(),
100            hash_id: s,
101        };
102    }
103}
104
105/// EkRoute provides a generic route which you can use to generate ephemeral (single use) encryption keys to bootstrap your request/response cycle within your application.
106///
107/// ### Setup
108///  1. Create a cache using one of the supported cache types:
109///
110///      ```rust
111///      use cached::{TimedCache, UnboundCache};
112///      use std::sync::{Arc, Mutex};
113///      use ncryptf::rocket::{EncryptionKey, CacheWrapper};
114///
115///      // TimedCache with 1 hour expiration
116///      let timed_cache = Arc::new(Mutex::new(TimedCache::with_lifespan(3600)));
117///      let cache_wrapper = CacheWrapper::TimedCache(timed_cache);
118///      
119///      // Or UnboundCache (no automatic expiration)
120///      let unbound_cache = Arc::new(Mutex::new(UnboundCache::new()));
121///      let cache_wrapper = CacheWrapper::UnboundCache(unbound_cache);
122///
123///      // Or RedisCache (requires redis feature)
124///      let redis_cache = Arc::new(Mutex::new(
125///          cached::RedisCache::new("redis://127.0.0.1/", std::time::Duration::from_secs(3600))
126///              .build().unwrap()
127///      ));
128///      let cache_wrapper = CacheWrapper::RedisCache(redis_cache);
129///      ```
130///
131///  2. Add the CacheWrapper as managed state to your Rocket instance:
132///
133///      ```rust
134///      let rocket = rocket::build()
135///          .manage(cache_wrapper)
136///          .mount("/ncryptf", routes![ncryptf_ek_route]);
137///      ```
138///
139///  3. Call the setup macro to instantiate the route:
140///
141///      ```rust
142///      ncryptf::ek_route!();
143///      ```
144///
145///  4. Mount the route `ncryptf_ek_route` exposed by the macro.
146///
147/// ### Features
148/// - **Unified Cache Interface**: Works with TimedCache, UnboundCache, and RedisCache through CacheWrapper
149/// - **Automatic Cache Management**: No need to manually handle different cache types
150/// - **Simple Integration**: Just manage a single CacheWrapper state instead of multiple cache types
151/// - **Parameterless Macro**: No arguments needed - the macro detects the managed cache automatically
152///
153/// Note: The CacheWrapper abstracts over all supported cache types: `TimedCache<String, EncryptionKey>`, `UnboundCache<String, EncryptionKey>`, and `RedisCache<String, EncryptionKey>`
154#[macro_export]
155macro_rules! ek_route {
156    () => {
157        use rocket::{get, http::Status, State};
158        use ncryptf::rocket::{EncryptionKey, ExportableEncryptionKeyData, CacheWrapper};
159        use base64::{Engine as _, engine::general_purpose};
160
161        #[get("/ek")]
162        pub async fn ncryptf_ek_route(
163            cache: &State<CacheWrapper>,
164        ) -> Result<ncryptf::rocket::Json<ExportableEncryptionKeyData>, Status> {
165            let ek = EncryptionKey::new(true);
166            
167            // Store the encryption key in the cache
168            cache.set(ek.get_hash_id(), ek.clone());
169
170            return Ok(ncryptf::rocket::Json(ExportableEncryptionKeyData {
171                public: general_purpose::STANDARD.encode(ek.get_box_kp().get_public_key()),
172                signature: general_purpose::STANDARD.encode(ek.get_sign_kp().get_public_key()),
173                hash_id: ek.get_hash_id(),
174                ephemeral: true,
175                expires_at: ek.expires_at,
176            }));
177        }
178    };
179}