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}