zerokms_protocol/
lib.rs

1mod base64_array;
2mod base64_vec;
3mod error;
4mod unverified_context;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::{borrow::Cow, fmt::Display, ops::Deref};
9use uuid::Uuid;
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12pub use cipherstash_config;
13/// Re-exports
14pub use error::*;
15
16pub use crate::unverified_context::{UnverifiedContext, UnverifiedContextValue};
17pub mod testing;
18
19#[async_trait]
20pub trait ViturConnection {
21    async fn send<Request: ViturRequest>(
22        &self,
23        request: Request,
24        access_token: &str,
25    ) -> Result<Request::Response, ViturRequestError>;
26}
27
28pub trait ViturResponse: Serialize + for<'de> Deserialize<'de> + Send {}
29
30#[async_trait]
31pub trait ViturRequest: Serialize + for<'de> Deserialize<'de> + Sized + Send {
32    type Response: ViturResponse;
33
34    const SCOPE: &'static str;
35    const ENDPOINT: &'static str;
36}
37
38/// Request message to create a new [Dataset] with the given name and description.
39///
40/// Requies the `dataset:create` scope.
41#[derive(Debug, Serialize, Deserialize)]
42pub struct CreateDatasetRequest<'a> {
43    pub name: Cow<'a, str>,
44    pub description: Cow<'a, str>,
45}
46
47impl ViturRequest for CreateDatasetRequest<'_> {
48    type Response = Dataset;
49
50    const ENDPOINT: &'static str = "create-dataset";
51    const SCOPE: &'static str = "dataset:create";
52}
53
54/// Request message to list all [Dataset]s.
55///
56/// Requires the `dataset:list` scope.
57/// Response is a vector of [Dataset]s.
58#[derive(Default, Debug, Serialize, Deserialize)]
59pub struct ListDatasetRequest {
60    #[serde(default)]
61    pub show_disabled: bool,
62}
63
64impl ViturRequest for ListDatasetRequest {
65    type Response = Vec<Dataset>;
66
67    const ENDPOINT: &'static str = "list-datasets";
68    const SCOPE: &'static str = "dataset:list";
69}
70
71/// Struct representing a dataset.
72/// This is the response to a [CreateDatasetRequest] and a in a vector in the response to a [ListDatasetRequest].
73#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
74pub struct Dataset {
75    pub id: Uuid,
76    pub name: String,
77    pub description: String,
78    pub is_disabled: bool,
79}
80
81impl ViturResponse for Dataset {}
82impl ViturResponse for Vec<Dataset> {}
83
84/// Represents an empty response for requests that don't return any data.
85#[derive(Default, Debug, Serialize, Deserialize)]
86pub struct EmptyResponse {}
87
88impl ViturResponse for EmptyResponse {}
89
90/// Request message to create a new client with the given name, description and dataset_id.
91///
92/// Requires the `client:create` scope.
93/// Response is a [CreateClientResponse].
94#[derive(Debug, Serialize, Deserialize)]
95pub struct CreateClientRequest<'a> {
96    pub dataset_id: Uuid,
97    pub name: Cow<'a, str>,
98    pub description: Cow<'a, str>,
99}
100
101impl ViturRequest for CreateClientRequest<'_> {
102    type Response = CreateClientResponse;
103
104    const ENDPOINT: &'static str = "create-client";
105    const SCOPE: &'static str = "client:create";
106}
107
108/// Response message to a [CreateClientRequest].
109///
110/// Contains the `client_id`` and the `client_key, the latter being a base64 encoded 32 byte key.
111/// The `client_key` should be considered sensitive and should be stored securely.
112#[derive(Debug, Serialize, Deserialize)]
113pub struct CreateClientResponse {
114    pub id: Uuid,
115    pub dataset_id: Uuid,
116    pub name: String,
117    pub description: String,
118    pub client_key: ViturKeyMaterial,
119}
120
121impl ViturResponse for CreateClientResponse {}
122
123/// Request message to list all clients.
124///
125/// Requires the `client:list` scope.
126/// Response is a vector of [DatasetClient]s.
127#[derive(Debug, Serialize, Deserialize)]
128pub struct ListClientRequest;
129
130impl ViturRequest for ListClientRequest {
131    type Response = Vec<DatasetClient>;
132
133    const ENDPOINT: &'static str = "list-clients";
134    const SCOPE: &'static str = "client:list";
135}
136
137/// Struct representing the dataset ids associated with a client
138/// which could be a single dataset or multiple datasets.
139#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
140#[serde(untagged)]
141pub enum ClientDatasetId {
142    Single(Uuid),
143    Multiple(Vec<Uuid>),
144}
145
146/// A `Uuid` is comparable with `ClientDatasetId` if the `ClientDatasetId` is a `Single` variant.
147impl PartialEq<Uuid> for ClientDatasetId {
148    fn eq(&self, other: &Uuid) -> bool {
149        if let ClientDatasetId::Single(id) = self {
150            id == other
151        } else {
152            false
153        }
154    }
155}
156
157/// Response type for a [ListClientRequest].
158#[derive(Debug, Serialize, Deserialize)]
159pub struct DatasetClient {
160    pub id: Uuid,
161    pub dataset_id: ClientDatasetId,
162    pub name: String,
163    pub description: String,
164}
165
166impl ViturResponse for Vec<DatasetClient> {}
167
168/// Request message to delete a client and all associated authority keys.
169///
170/// Requires the `client:revoke` scope.
171/// Response is an [DeleteClientResponse].
172#[derive(Debug, Serialize, Deserialize)]
173pub struct DeleteClientRequest {
174    pub client_id: Uuid,
175}
176
177impl ViturRequest for DeleteClientRequest {
178    type Response = DeleteClientResponse;
179
180    const ENDPOINT: &'static str = "delete-client";
181    const SCOPE: &'static str = "client:delete";
182}
183
184#[derive(Default, Debug, Serialize, Deserialize)]
185pub struct DeleteClientResponse {}
186
187impl ViturResponse for DeleteClientResponse {}
188
189/// Key material type used in [GenerateKeyRequest] and [RetrieveKeyRequest] as well as [CreateClientResponse].
190#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
191pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
192opaque_debug::implement!(ViturKeyMaterial);
193
194impl From<Vec<u8>> for ViturKeyMaterial {
195    fn from(inner: Vec<u8>) -> Self {
196        Self(inner)
197    }
198}
199
200impl Deref for ViturKeyMaterial {
201    type Target = [u8];
202
203    fn deref(&self) -> &Self::Target {
204        &self.0
205    }
206}
207
208#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
209#[serde(transparent)]
210pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
211
212impl KeyId {
213    pub fn into_inner(self) -> [u8; 16] {
214        self.0
215    }
216}
217
218impl Display for KeyId {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(f, "{}", const_hex::encode(self.0))
221    }
222}
223
224impl From<[u8; 16]> for KeyId {
225    fn from(inner: [u8; 16]) -> Self {
226        Self(inner)
227    }
228}
229
230impl AsRef<[u8; 16]> for KeyId {
231    fn as_ref(&self) -> &[u8; 16] {
232        &self.0
233    }
234}
235
236/// Represents generated data key material which is used by the client to derive data keys with its own key material.
237///
238/// Returned in the response to a [GenerateKeyRequest].
239#[derive(Debug, Serialize, Deserialize)]
240pub struct GeneratedKey {
241    pub key_material: ViturKeyMaterial,
242    // FIXME: Use Vitamin C Equatable type
243    #[serde(with = "base64_vec")]
244    pub tag: Vec<u8>,
245}
246
247/// Response to a [GenerateKeyRequest].
248#[derive(Debug, Serialize, Deserialize)]
249pub struct GenerateKeyResponse {
250    pub keys: Vec<GeneratedKey>,
251}
252
253impl ViturResponse for GenerateKeyResponse {}
254
255/// A specification for generating a data key used in a [GenerateKeyRequest].
256#[derive(Debug, Serialize, Deserialize, Clone)]
257pub struct GenerateKeySpec<'a> {
258    // FIXME: Remove ID and have the server generate it instead
259    #[serde(alias = "id")]
260    pub iv: KeyId,
261    // TODO: Deprecate descriptor in favor of context
262    pub descriptor: Cow<'a, str>,
263
264    #[serde(default = "Vec::new")]
265    pub context: Vec<Context>,
266}
267
268impl<'a> GenerateKeySpec<'a> {
269    pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
270        Self {
271            iv: KeyId(iv),
272            descriptor: Cow::from(descriptor),
273            context: Vec::new(),
274        }
275    }
276
277    pub fn new_with_context(iv: [u8; 16], descriptor: &'a str, context: Vec<Context>) -> Self {
278        Self {
279            iv: KeyId(iv),
280            descriptor: Cow::from(descriptor),
281            context,
282        }
283    }
284}
285/// Represents a contextual attribute for a data key which is used to "lock" the key to a specific context.
286/// Context attributes are included key tag generation which is in turn used as AAD in the final encryption step in the client.
287/// Context attributes should _never_ include any sensitive information.
288#[derive(Debug, Serialize, Deserialize, Clone)]
289// TODO: Use Cow?
290pub enum Context {
291    /// A tag that can be used to identify the key.
292    Tag(String),
293
294    /// A key-value pair that can be used to identify the key.
295    /// For example, a key-value pair could be `("user_id", "1234")`.
296    Value(String, String),
297
298    /// A claim from the identity of the principal that is requesting the key.
299    /// The claim value is read from the claims list after token verification and prior to key generation.
300    ///
301    /// For example, a claim could be `"sub"`.
302    IdentityClaim(String),
303}
304
305impl Context {
306    pub fn new_tag(tag: impl Into<String>) -> Self {
307        Self::Tag(tag.into())
308    }
309
310    pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
311        Self::Value(key.into(), value.into())
312    }
313
314    pub fn new_identity_claim(claim: &str) -> Self {
315        Self::IdentityClaim(claim.to_string())
316    }
317}
318
319/// A request message to generate a data key made on behalf of a client
320/// in the given dataset.
321///
322/// Requires the `data_key:generate` scope.
323/// Response is a [GenerateKeyResponse].
324///
325/// See also [GenerateKeySpec].
326#[derive(Debug, Serialize, Deserialize)]
327pub struct GenerateKeyRequest<'a> {
328    pub client_id: Uuid,
329    pub dataset_id: Option<Uuid>,
330    pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
331    #[serde(default)]
332    pub unverified_context: UnverifiedContext,
333}
334
335impl ViturRequest for GenerateKeyRequest<'_> {
336    type Response = GenerateKeyResponse;
337
338    const ENDPOINT: &'static str = "generate-data-key";
339    const SCOPE: &'static str = "data_key:generate";
340}
341
342/// Returned type from a [RetrieveKeyRequest].
343#[derive(Debug, Serialize, Deserialize)]
344pub struct RetrievedKey {
345    pub key_material: ViturKeyMaterial,
346}
347
348/// Response to a [RetrieveKeyRequest].
349/// Contains a list of [RetrievedKey]s.
350#[derive(Debug, Serialize, Deserialize)]
351pub struct RetrieveKeyResponse {
352    pub keys: Vec<RetrievedKey>,
353}
354
355impl ViturResponse for RetrieveKeyResponse {}
356
357/// A specification for retrieving a data key used in a [RetrieveKeyRequest].
358#[derive(Debug, Serialize, Deserialize, Clone)]
359pub struct RetrieveKeySpec<'a> {
360    #[serde(alias = "id")]
361    pub iv: KeyId,
362    // TODO: Make Descriptor Optional
363    pub descriptor: Cow<'a, str>,
364    pub tag: Cow<'a, [u8]>,
365
366    #[serde(default = "Vec::new")]
367    pub context: Vec<Context>,
368
369    // Since this field will be removed in the future allow older versions of Vitur to be able to
370    // parse a RetrieveKeySpec that doesn't include the tag_version.
371    #[serde(default)]
372    pub tag_version: usize,
373}
374
375impl<'a> RetrieveKeySpec<'a> {
376    const DEFAULT_TAG_VERSION: usize = 0;
377
378    pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
379        Self {
380            iv: id,
381            descriptor: Cow::from(descriptor),
382            tag: Cow::from(tag),
383            context: Vec::new(),
384            tag_version: Self::DEFAULT_TAG_VERSION,
385        }
386    }
387
388    pub fn with_context(mut self, context: Vec<Context>) -> Self {
389        self.context = context;
390        self
391    }
392}
393
394/// Request to retrieve a data key on behalf of a client in the given dataset.
395/// Requires the `data_key:retrieve` scope.
396/// Response is a [RetrieveKeyResponse].
397///
398/// See also [RetrieveKeySpec].
399#[derive(Debug, Serialize, Deserialize)]
400pub struct RetrieveKeyRequest<'a> {
401    pub client_id: Uuid,
402    pub dataset_id: Option<Uuid>,
403    pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
404    #[serde(default)]
405    pub unverified_context: UnverifiedContext,
406}
407
408impl ViturRequest for RetrieveKeyRequest<'_> {
409    type Response = RetrieveKeyResponse;
410
411    const ENDPOINT: &'static str = "retrieve-data-key";
412    const SCOPE: &'static str = "data_key:retrieve";
413}
414
415/// Request to retrieve a data key on behalf of a client in the given dataset.
416/// Requires the `data_key:retrieve` scope.
417/// Response is a [RetrieveKeyResponse].
418///
419/// See also [RetrieveKeySpec].
420#[derive(Debug, Serialize, Deserialize)]
421pub struct RetrieveKeyRequestFallible<'a> {
422    pub client_id: Uuid,
423    pub dataset_id: Option<Uuid>,
424    pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
425    #[serde(default)]
426    pub unverified_context: UnverifiedContext,
427}
428
429impl ViturRequest for RetrieveKeyRequestFallible<'_> {
430    type Response = RetrieveKeyResponseFallible;
431
432    const ENDPOINT: &'static str = "retrieve-data-key-fallible";
433    const SCOPE: &'static str = "data_key:retrieve";
434}
435
436/// Response to a [RetrieveKeyRequest] with per-key error handling
437#[derive(Debug, Serialize, Deserialize)]
438pub struct RetrieveKeyResponseFallible {
439    pub keys: Vec<Result<RetrievedKey, String>>, // TODO: Error?
440}
441
442impl ViturResponse for RetrieveKeyResponseFallible {}
443
444/// Request message to disable a dataset.
445/// Requires the `dataset:disable` scope.
446/// Response is an [EmptyResponse].
447#[derive(Debug, Serialize, Deserialize)]
448pub struct DisableDatasetRequest {
449    pub dataset_id: Uuid,
450}
451
452impl ViturRequest for DisableDatasetRequest {
453    type Response = EmptyResponse;
454
455    const ENDPOINT: &'static str = "disable-dataset";
456    const SCOPE: &'static str = "dataset:disable";
457}
458
459/// Request message to enable a dataset that has was previously disabled.
460/// Requires the `dataset:enable` scope.
461/// Response is an [EmptyResponse].
462#[derive(Debug, Serialize, Deserialize)]
463pub struct EnableDatasetRequest {
464    pub dataset_id: Uuid,
465}
466
467impl ViturRequest for EnableDatasetRequest {
468    type Response = EmptyResponse;
469
470    const ENDPOINT: &'static str = "enable-dataset";
471    const SCOPE: &'static str = "dataset:enable";
472}
473
474/// Request message to modify a dataset with the given dataset_id.
475/// `name` and `description` are optional and will be updated if provided.
476///
477/// Requires the `dataset:modify` scope.
478/// Response is an [EmptyResponse].
479#[derive(Debug, Serialize, Deserialize)]
480pub struct ModifyDatasetRequest<'a> {
481    pub dataset_id: Uuid,
482
483    pub name: Option<Cow<'a, str>>,
484    pub description: Option<Cow<'a, str>>,
485}
486
487impl ViturRequest for ModifyDatasetRequest<'_> {
488    type Response = EmptyResponse;
489
490    const ENDPOINT: &'static str = "modify-dataset";
491    const SCOPE: &'static str = "dataset:modify";
492}
493
494/// Request message to grant a client access to a dataset.
495/// Requires the `dataset:grant` scope.
496///
497/// Response is an [EmptyResponse].
498#[derive(Debug, Serialize, Deserialize)]
499pub struct GrantDatasetRequest {
500    pub client_id: Uuid,
501    pub dataset_id: Uuid,
502}
503
504impl ViturRequest for GrantDatasetRequest {
505    type Response = EmptyResponse;
506
507    const ENDPOINT: &'static str = "grant-dataset";
508    const SCOPE: &'static str = "dataset:grant";
509}
510
511/// Request message to revoke a client's access to a dataset.
512/// Requires the `dataset:revoke` scope.
513/// Response is an [EmptyResponse].
514#[derive(Debug, Serialize, Deserialize)]
515pub struct RevokeDatasetRequest {
516    pub client_id: Uuid,
517    pub dataset_id: Uuid,
518}
519
520impl ViturRequest for RevokeDatasetRequest {
521    type Response = EmptyResponse;
522
523    const ENDPOINT: &'static str = "revoke-dataset";
524    const SCOPE: &'static str = "dataset:revoke";
525}
526
527/// Request to load a dataset on behalf of a client.
528/// This is used by clients before indexing or querying data and includes
529/// key material which can be derived by the client to generate encrypted index terms.
530///
531/// If a dataset_id is not provided the client's default dataset will be loaded.
532///
533/// Requires the `data_key:retrieve` scope (though this may change in the future).
534/// Response is a [LoadDatasetResponse].
535#[derive(Debug, Serialize, Deserialize)]
536pub struct LoadDatasetRequest {
537    pub client_id: Uuid,
538    pub dataset_id: Option<Uuid>,
539}
540
541impl ViturRequest for LoadDatasetRequest {
542    type Response = LoadDatasetResponse;
543
544    const ENDPOINT: &'static str = "load-dataset";
545    // NOTE: We don't currently support the ability to allow an operation
546    // based on any one of several possible scopes so we'll just use `data_key:retrieve` for now.
547    // This should probably be allowed for any operation that requires indexing or querying.
548    const SCOPE: &'static str = "data_key:retrieve";
549}
550
551/// Response to a [LoadDatasetRequest].
552/// The response includes the key material required to derive data keys.
553/// It is analogous to a [RetrieveKeyResponse] but where the server generated the key.
554#[derive(Debug, Serialize, Deserialize)]
555pub struct LoadDatasetResponse {
556    pub partial_index_key: RetrievedKey,
557    pub dataset: Dataset,
558}
559
560impl ViturResponse for LoadDatasetResponse {}