Skip to main content

zerokms_protocol/
lib.rs

1mod base64_array;
2mod base64_vec;
3mod error;
4mod identified_by;
5
6use cts_common::claims::{
7    ClientPermission, DataKeyPermission, KeysetPermission, Permission, Scope,
8};
9pub use identified_by::*;
10
11mod unverified_context;
12
13use serde::{Deserialize, Serialize};
14use std::{
15    borrow::Cow,
16    fmt::{self, Debug, Display, Formatter},
17    ops::Deref,
18};
19use utoipa::ToSchema;
20use uuid::Uuid;
21use zeroize::{Zeroize, ZeroizeOnDrop};
22
23pub use cipherstash_config;
24/// Re-exports
25pub use error::*;
26
27pub use crate::unverified_context::{UnverifiedContext, UnverifiedContextValue};
28pub use crate::{IdentifiedBy, Name};
29pub mod testing;
30
31pub trait ViturResponse: Serialize + for<'de> Deserialize<'de> + Send {}
32
33pub trait ViturRequest: Serialize + for<'de> Deserialize<'de> + Sized + Send {
34    type Response: ViturResponse;
35
36    const SCOPE: Scope;
37    const ENDPOINT: &'static str;
38}
39
40/// The type of client to create.
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
42#[serde(rename_all = "snake_case")]
43pub enum ClientType {
44    Device,
45}
46
47/// Specification for creating a client alongside a keyset.
48#[derive(Debug, Serialize, Deserialize, ToSchema)]
49pub struct CreateClientSpec<'a> {
50    pub client_type: ClientType,
51    /// A human-readable name for the client.
52    #[schema(value_type = String)]
53    pub name: Cow<'a, str>,
54}
55
56/// Details of a client created as part of a [CreateKeysetRequest].
57#[derive(Debug, Serialize, Deserialize, ToSchema)]
58pub struct CreatedClient {
59    pub id: Uuid,
60    /// Base64-encoded 32-byte key material for the client. Store this securely.
61    #[schema(value_type = String, format = Byte)]
62    pub client_key: ViturKeyMaterial,
63}
64
65/// Response to a [CreateKeysetRequest].
66///
67/// Contains the created keyset and optionally a client if one was requested.
68#[derive(Debug, Serialize, Deserialize, ToSchema)]
69pub struct CreateKeysetResponse {
70    #[serde(flatten)]
71    pub keyset: Keyset,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub client: Option<CreatedClient>,
74}
75
76impl ViturResponse for CreateKeysetResponse {}
77
78/// Request message to create a new [Keyset] with the given name and description.
79///
80/// Requies the `dataset:create` scope.
81#[derive(Debug, Serialize, Deserialize, ToSchema)]
82pub struct CreateKeysetRequest<'a> {
83    /// A human-readable name for the keyset.
84    #[schema(value_type = String)]
85    pub name: Cow<'a, str>,
86    /// A description of the keyset.
87    #[schema(value_type = String)]
88    pub description: Cow<'a, str>,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub client: Option<CreateClientSpec<'a>>,
91}
92
93impl ViturRequest for CreateKeysetRequest<'_> {
94    type Response = CreateKeysetResponse;
95
96    const ENDPOINT: &'static str = "create-keyset";
97    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Create));
98}
99
100/// Request message to list all [Keyset]s.
101///
102/// Requires the `dataset:list` scope.
103/// Response is a vector of [Keyset]s.
104#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
105pub struct ListKeysetRequest {
106    #[serde(default)]
107    pub show_disabled: bool,
108}
109
110impl ViturRequest for ListKeysetRequest {
111    type Response = Vec<Keyset>;
112
113    const ENDPOINT: &'static str = "list-keysets";
114    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::List));
115}
116
117/// Struct representing a keyset.
118/// This is the response to a [CreateKeysetRequest] and a in a vector in the response to a [ListKeysetRequest].
119#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
120pub struct Keyset {
121    pub id: Uuid,
122    pub name: String,
123    pub description: String,
124    pub is_disabled: bool,
125    #[serde(default)]
126    pub is_default: bool,
127}
128
129impl ViturResponse for Vec<Keyset> {}
130
131/// Represents an empty response for requests that don't return any data.
132#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
133pub struct EmptyResponse {}
134
135impl ViturResponse for EmptyResponse {}
136
137/// Request message to create a new client with the given name and description.
138///
139/// If `keyset_id` is omitted, the workspace's default keyset is used (created if necessary).
140///
141/// Requires the `client:create` scope.
142/// Response is a [CreateClientResponse].
143#[derive(Debug, Serialize, Deserialize, ToSchema)]
144pub struct CreateClientRequest<'a> {
145    /// The keyset to associate the client with. Accepts a UUID or a name string.
146    /// If omitted, the workspace's default keyset is used.
147    #[serde(alias = "dataset_id", default, skip_serializing_if = "Option::is_none")]
148    #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
149    pub keyset_id: Option<IdentifiedBy>,
150    /// A human-readable name for the client.
151    #[schema(value_type = String)]
152    pub name: Cow<'a, str>,
153    /// A description of the client.
154    #[schema(value_type = String)]
155    pub description: Cow<'a, str>,
156}
157
158impl ViturRequest for CreateClientRequest<'_> {
159    type Response = CreateClientResponse;
160
161    const ENDPOINT: &'static str = "create-client";
162    const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Create));
163}
164
165/// Response message to a [CreateClientRequest].
166///
167/// Contains the `client_id` and the `client_key`, the latter being a base64 encoded 32 byte key.
168/// The `client_key` should be considered sensitive and should be stored securely.
169#[derive(Debug, Serialize, Deserialize, ToSchema)]
170pub struct CreateClientResponse {
171    /// The unique ID of the newly created client.
172    pub id: Uuid,
173    /// The ID of the keyset this client is associated with.
174    #[serde(rename = "dataset_id")]
175    pub keyset_id: Uuid,
176    /// The name of the client.
177    pub name: String,
178    /// The description of the client.
179    pub description: String,
180    /// Base64-encoded 32-byte key material for the client. Store this securely.
181    #[schema(value_type = String, format = Byte)]
182    pub client_key: ViturKeyMaterial,
183}
184
185impl ViturResponse for CreateClientResponse {}
186
187/// Request message to list all clients.
188///
189/// Requires the `client:list` scope.
190/// Response is a vector of [KeysetClient]s.
191#[derive(Debug, Serialize, Deserialize)]
192pub struct ListClientRequest;
193
194impl ViturRequest for ListClientRequest {
195    type Response = Vec<KeysetClient>;
196
197    const ENDPOINT: &'static str = "list-clients";
198    const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::List));
199}
200
201/// Struct representing the keyset ids associated with a client
202/// which could be a single keyset or multiple keysets.
203#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema)]
204#[serde(untagged)]
205pub enum ClientKeysetId {
206    Single(Uuid),
207    Multiple(Vec<Uuid>),
208}
209
210/// A `Uuid` is comparable with `ClientKeysetId` if the `ClientKeysetId` is a `Single` variant.
211impl PartialEq<Uuid> for ClientKeysetId {
212    fn eq(&self, other: &Uuid) -> bool {
213        if let ClientKeysetId::Single(id) = self {
214            id == other
215        } else {
216            false
217        }
218    }
219}
220
221/// Response type for a [ListClientRequest].
222#[derive(Debug, Serialize, Deserialize, ToSchema)]
223pub struct KeysetClient {
224    pub id: Uuid,
225    #[serde(alias = "dataset_id")]
226    pub keyset_id: ClientKeysetId,
227    pub name: String,
228    pub description: String,
229}
230
231impl ViturResponse for Vec<KeysetClient> {}
232
233/// Request message to delete a client and all associated authority keys.
234///
235/// Requires the `client:revoke` scope.
236/// Response is an [DeleteClientResponse].
237#[derive(Debug, Serialize, Deserialize, ToSchema)]
238pub struct DeleteClientRequest {
239    pub client_id: Uuid,
240}
241
242impl ViturRequest for DeleteClientRequest {
243    type Response = DeleteClientResponse;
244
245    const ENDPOINT: &'static str = "delete-client";
246    const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Delete));
247}
248
249#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
250pub struct DeleteClientResponse {}
251
252impl ViturResponse for DeleteClientResponse {}
253
254/// Key material type used in [GenerateKeyRequest] and [RetrieveKeyRequest] as well as [CreateClientResponse].
255#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
256pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
257opaque_debug::implement!(ViturKeyMaterial);
258
259impl From<Vec<u8>> for ViturKeyMaterial {
260    fn from(inner: Vec<u8>) -> Self {
261        Self(inner)
262    }
263}
264
265impl Deref for ViturKeyMaterial {
266    type Target = [u8];
267
268    fn deref(&self) -> &Self::Target {
269        &self.0
270    }
271}
272
273#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
274#[serde(transparent)]
275pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
276
277impl KeyId {
278    pub fn into_inner(self) -> [u8; 16] {
279        self.0
280    }
281}
282
283impl Display for KeyId {
284    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
285        write!(f, "{}", const_hex::encode(self.0))
286    }
287}
288
289impl From<[u8; 16]> for KeyId {
290    fn from(inner: [u8; 16]) -> Self {
291        Self(inner)
292    }
293}
294
295impl AsRef<[u8; 16]> for KeyId {
296    fn as_ref(&self) -> &[u8; 16] {
297        &self.0
298    }
299}
300
301/// Represents generated data key material which is used by the client to derive data keys with its own key material.
302///
303/// Returned in the response to a [GenerateKeyRequest].
304#[derive(Debug, Serialize, Deserialize, ToSchema)]
305pub struct GeneratedKey {
306    #[schema(value_type = String, format = Byte)]
307    pub key_material: ViturKeyMaterial,
308    // FIXME: Use Vitamin C Equatable type
309    #[serde(with = "base64_vec")]
310    #[schema(value_type = String, format = Byte)]
311    pub tag: Vec<u8>,
312}
313
314/// Response to a [GenerateKeyRequest].
315#[derive(Debug, Serialize, Deserialize, ToSchema)]
316pub struct GenerateKeyResponse {
317    pub keys: Vec<GeneratedKey>,
318}
319
320impl ViturResponse for GenerateKeyResponse {}
321
322/// A specification for generating a data key used in a [GenerateKeyRequest].
323#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
324pub struct GenerateKeySpec<'a> {
325    // FIXME: Remove ID and have the server generate it instead
326    #[serde(alias = "id")]
327    #[schema(value_type = String, format = Byte)]
328    pub iv: KeyId,
329    // TODO: Deprecate descriptor in favor of context
330    #[schema(value_type = String)]
331    pub descriptor: Cow<'a, str>,
332
333    #[serde(default)]
334    #[schema(value_type = Vec<Context>)]
335    pub context: Cow<'a, [Context]>,
336}
337
338impl<'a> GenerateKeySpec<'a> {
339    pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
340        Self {
341            iv: KeyId(iv),
342            descriptor: Cow::from(descriptor),
343            context: Default::default(),
344        }
345    }
346
347    pub fn new_with_context(
348        iv: [u8; 16],
349        descriptor: &'a str,
350        context: Cow<'a, [Context]>,
351    ) -> Self {
352        Self {
353            iv: KeyId(iv),
354            descriptor: Cow::from(descriptor),
355            context,
356        }
357    }
358}
359/// Represents a contextual attribute for a data key which is used to "lock" the key to a specific context.
360/// Context attributes are included key tag generation which is in turn used as AAD in the final encryption step in the client.
361/// Context attributes should _never_ include any sensitive information.
362#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
363// TODO: Use Cow?
364pub enum Context {
365    /// A tag that can be used to identify the key.
366    Tag(String),
367
368    /// A key-value pair that can be used to identify the key.
369    /// For example, a key-value pair could be `("user_id", "1234")`.
370    Value(String, String),
371
372    /// A claim from the identity of the principal that is requesting the key.
373    /// The claim value is read from the claims list after token verification and prior to key generation.
374    ///
375    /// For example, a claim could be `"sub"`.
376    #[serde(alias = "identityClaim")]
377    IdentityClaim(String),
378}
379
380impl Context {
381    pub fn new_tag(tag: impl Into<String>) -> Self {
382        Self::Tag(tag.into())
383    }
384
385    pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
386        Self::Value(key.into(), value.into())
387    }
388
389    pub fn new_identity_claim(claim: &str) -> Self {
390        Self::IdentityClaim(claim.to_string())
391    }
392}
393
394/// A request message to generate a data key made on behalf of a client
395/// in the given keyset.
396///
397/// Requires the `data_key:generate` scope.
398/// Response is a [GenerateKeyResponse].
399///
400/// See also [GenerateKeySpec].
401#[derive(Debug, Serialize, Deserialize, ToSchema)]
402pub struct GenerateKeyRequest<'a> {
403    pub client_id: Uuid,
404    #[serde(alias = "dataset_id")]
405    #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
406    pub keyset_id: Option<IdentifiedBy>,
407    #[schema(value_type = Vec<GenerateKeySpec>)]
408    pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
409    #[serde(default)]
410    #[schema(value_type = Object)]
411    pub unverified_context: Cow<'a, UnverifiedContext>,
412}
413
414impl ViturRequest for GenerateKeyRequest<'_> {
415    type Response = GenerateKeyResponse;
416
417    const ENDPOINT: &'static str = "generate-data-key";
418    const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Generate));
419}
420
421/// Returned type from a [RetrieveKeyRequest].
422#[derive(Debug, Serialize, Deserialize, ToSchema)]
423pub struct RetrievedKey {
424    /// Base64-encoded key material.
425    #[schema(value_type = String, format = Byte)]
426    pub key_material: ViturKeyMaterial,
427}
428
429/// Response to a [RetrieveKeyRequest].
430/// Contains a list of [RetrievedKey]s.
431#[derive(Debug, Serialize, Deserialize, ToSchema)]
432pub struct RetrieveKeyResponse {
433    pub keys: Vec<RetrievedKey>,
434}
435
436impl ViturResponse for RetrieveKeyResponse {}
437
438/// A specification for retrieving a data key used in a [RetrieveKeyRequest].
439#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
440pub struct RetrieveKeySpec<'a> {
441    #[serde(alias = "id")]
442    #[schema(value_type = String, format = Byte)]
443    pub iv: KeyId,
444    // TODO: Make Descriptor Optional
445    #[schema(value_type = String)]
446    pub descriptor: Cow<'a, str>,
447    #[schema(value_type = String, format = Byte)]
448    pub tag: Cow<'a, [u8]>,
449
450    #[serde(default)]
451    #[schema(value_type = Vec<Context>)]
452    pub context: Cow<'a, [Context]>,
453
454    // Since this field will be removed in the future allow older versions of Vitur to be able to
455    // parse a RetrieveKeySpec that doesn't include the tag_version.
456    #[serde(default)]
457    pub tag_version: usize,
458}
459
460impl<'a> RetrieveKeySpec<'a> {
461    const DEFAULT_TAG_VERSION: usize = 0;
462
463    pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
464        Self {
465            iv: id,
466            descriptor: Cow::from(descriptor),
467            tag: Cow::from(tag),
468            context: Cow::Owned(Vec::new()),
469            tag_version: Self::DEFAULT_TAG_VERSION,
470        }
471    }
472
473    pub fn with_context(mut self, context: Cow<'a, [Context]>) -> Self {
474        self.context = context;
475        self
476    }
477}
478
479/// Request to retrieve a data key on behalf of a client in the given keyset.
480/// Requires the `data_key:retrieve` scope.
481/// Response is a [RetrieveKeyResponse].
482///
483/// See also [RetrieveKeySpec].
484#[derive(Debug, Serialize, Deserialize, ToSchema)]
485pub struct RetrieveKeyRequest<'a> {
486    pub client_id: Uuid,
487    #[serde(alias = "dataset_id")]
488    #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
489    pub keyset_id: Option<IdentifiedBy>,
490    #[schema(value_type = Vec<RetrieveKeySpec>)]
491    pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
492    #[serde(default)]
493    #[schema(value_type = Object)]
494    pub unverified_context: UnverifiedContext,
495}
496
497impl ViturRequest for RetrieveKeyRequest<'_> {
498    type Response = RetrieveKeyResponse;
499
500    const ENDPOINT: &'static str = "retrieve-data-key";
501    const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
502}
503
504/// Request to retrieve a data key on behalf of a client in the given keyset.
505/// Requires the `data_key:retrieve` scope.
506/// Response is a [RetrieveKeyResponse].
507///
508/// See also [RetrieveKeySpec].
509#[derive(Debug, Serialize, Deserialize)]
510pub struct RetrieveKeyRequestFallible<'a> {
511    pub client_id: Uuid,
512    #[serde(alias = "dataset_id")]
513    pub keyset_id: Option<IdentifiedBy>,
514    pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
515    #[serde(default)]
516    pub unverified_context: Cow<'a, UnverifiedContext>,
517}
518
519impl ViturRequest for RetrieveKeyRequestFallible<'_> {
520    type Response = RetrieveKeyResponseFallible;
521
522    const ENDPOINT: &'static str = "retrieve-data-key-fallible";
523    const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
524}
525
526/// Response to a [RetrieveKeyRequest] with per-key error handling
527#[derive(Debug, Serialize, Deserialize, ToSchema)]
528pub struct RetrieveKeyResponseFallible {
529    #[schema(value_type = Vec<serde_json::Value>)]
530    pub keys: Vec<Result<RetrievedKey, String>>, // TODO: Error?
531}
532
533impl ViturResponse for RetrieveKeyResponseFallible {}
534
535/// Request message to disable a keyset.
536/// Requires the `dataset:disable` scope.
537/// Response is an [EmptyResponse].
538#[derive(Debug, Serialize, Deserialize, ToSchema)]
539pub struct DisableKeysetRequest {
540    /// The keyset to disable. Accepts a UUID or a name string.
541    #[serde(alias = "dataset_id")]
542    #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
543    pub keyset_id: IdentifiedBy,
544}
545
546impl ViturRequest for DisableKeysetRequest {
547    type Response = EmptyResponse;
548
549    const ENDPOINT: &'static str = "disable-keyset";
550    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Disable));
551}
552
553/// Request message to enable a keyset that has was previously disabled.
554/// Requires the `dataset:enable` scope.
555/// Response is an [EmptyResponse].
556#[derive(Debug, Serialize, Deserialize, ToSchema)]
557pub struct EnableKeysetRequest {
558    /// The keyset to enable. Accepts a UUID or a name string.
559    #[serde(alias = "dataset_id")]
560    #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
561    pub keyset_id: IdentifiedBy,
562}
563
564impl ViturRequest for EnableKeysetRequest {
565    type Response = EmptyResponse;
566
567    const ENDPOINT: &'static str = "enable-keyset";
568    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Enable));
569}
570
571/// Request message to modify a keyset with the given keyset_id.
572/// `name` and `description` are optional and will be updated if provided.
573///
574/// Requires the `dataset:modify` scope.
575/// Response is an [EmptyResponse].
576#[derive(Debug, Serialize, Deserialize, ToSchema)]
577pub struct ModifyKeysetRequest<'a> {
578    /// The keyset to modify. Accepts a UUID or a name string.
579    #[serde(alias = "dataset_id")]
580    #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
581    pub keyset_id: IdentifiedBy,
582    /// Optional new name for the keyset.
583    #[schema(value_type = Option<String>)]
584    pub name: Option<Cow<'a, str>>,
585    /// Optional new description for the keyset.
586    #[schema(value_type = Option<String>)]
587    pub description: Option<Cow<'a, str>>,
588}
589
590impl ViturRequest for ModifyKeysetRequest<'_> {
591    type Response = EmptyResponse;
592
593    const ENDPOINT: &'static str = "modify-keyset";
594    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Modify));
595}
596
597/// Request message to grant a client access to a keyset.
598/// Requires the `dataset:grant` scope.
599///
600/// Response is an [EmptyResponse].
601#[derive(Debug, Serialize, Deserialize, ToSchema)]
602pub struct GrantKeysetRequest {
603    pub client_id: Uuid,
604    /// The keyset to grant access to. Accepts a UUID or a name string.
605    #[serde(alias = "dataset_id")]
606    #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
607    pub keyset_id: IdentifiedBy,
608}
609
610impl ViturRequest for GrantKeysetRequest {
611    type Response = EmptyResponse;
612
613    const ENDPOINT: &'static str = "grant-keyset";
614    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Grant));
615}
616
617/// Request message to revoke a client's access to a keyset.
618/// Requires the `dataset:revoke` scope.
619/// Response is an [EmptyResponse].
620#[derive(Debug, Serialize, Deserialize, ToSchema)]
621pub struct RevokeKeysetRequest {
622    pub client_id: Uuid,
623    /// The keyset to revoke access from. Accepts a UUID or a name string.
624    #[serde(alias = "dataset_id")]
625    #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
626    pub keyset_id: IdentifiedBy,
627}
628
629impl ViturRequest for RevokeKeysetRequest {
630    type Response = EmptyResponse;
631
632    const ENDPOINT: &'static str = "revoke-keyset";
633    const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Revoke));
634}
635
636/// Request to load a keyset on behalf of a client.
637/// This is used by clients before indexing or querying data and includes
638/// key material which can be derived by the client to generate encrypted index terms.
639///
640/// If a keyset_id is not provided the client's default keyset will be loaded.
641///
642/// Requires the `data_key:retrieve` scope (though this may change in the future).
643/// Response is a [LoadKeysetResponse].
644#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, ToSchema)]
645pub struct LoadKeysetRequest {
646    pub client_id: Uuid,
647    /// The keyset to load. Accepts a UUID or a name string. If omitted, the client's default keyset is used.
648    #[serde(alias = "dataset_id")]
649    #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
650    pub keyset_id: Option<IdentifiedBy>,
651}
652
653impl ViturRequest for LoadKeysetRequest {
654    type Response = LoadKeysetResponse;
655
656    const ENDPOINT: &'static str = "load-keyset";
657
658    // NOTE: We don't currently support the ability to allow an operation
659    // based on any one of several possible scopes so we'll just use `data_key:retrieve` for now.
660    // This should probably be allowed for any operation that requires indexing or querying.
661    const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
662}
663
664/// Response to a [LoadKeysetRequest].
665/// The response includes the key material required to derive data keys.
666/// It is analogous to a [RetrieveKeyResponse] but where the server generated the key.
667#[derive(Debug, Serialize, Deserialize, ToSchema)]
668pub struct LoadKeysetResponse {
669    pub partial_index_key: RetrievedKey,
670    #[serde(rename = "dataset")]
671    pub keyset: Keyset,
672}
673
674impl ViturResponse for LoadKeysetResponse {}
675
676#[cfg(test)]
677mod test {
678    use serde_json::json;
679    use uuid::Uuid;
680
681    use crate::{CreateKeysetResponse, CreatedClient, IdentifiedBy, LoadKeysetRequest, Name};
682
683    mod create_keyset_response_serialization {
684        use super::*;
685        use crate::{Keyset, ViturKeyMaterial};
686
687        #[test]
688        fn without_client_is_flat_keyset() {
689            let id = Uuid::new_v4();
690            let response = CreateKeysetResponse {
691                keyset: Keyset {
692                    id,
693                    name: "test-keyset".into(),
694                    description: "A test keyset".into(),
695                    is_disabled: false,
696                    is_default: false,
697                },
698                client: None,
699            };
700
701            let serialized = serde_json::to_value(&response).unwrap();
702
703            // Should be a flat object identical to the old Keyset response
704            assert_eq!(
705                serialized,
706                json!({
707                    "id": id,
708                    "name": "test-keyset",
709                    "description": "A test keyset",
710                    "is_disabled": false,
711                    "is_default": false,
712                })
713            );
714
715            // Should round-trip
716            let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
717            assert_eq!(deserialized.keyset.id, id);
718            assert!(deserialized.client.is_none());
719        }
720
721        #[test]
722        fn with_client_includes_client_field() {
723            let keyset_id = Uuid::new_v4();
724            let client_id = Uuid::new_v4();
725
726            let response = CreateKeysetResponse {
727                keyset: Keyset {
728                    id: keyset_id,
729                    name: "device-keyset".into(),
730                    description: "Keyset with device client".into(),
731                    is_disabled: false,
732                    is_default: false,
733                },
734                client: Some(CreatedClient {
735                    id: client_id,
736                    client_key: ViturKeyMaterial::from(vec![1, 2, 3, 4]),
737                }),
738            };
739
740            let serialized = serde_json::to_value(&response).unwrap();
741
742            // Keyset fields are flat, client is a nested object with base64-encoded key
743            assert_eq!(
744                serialized,
745                json!({
746                    "id": keyset_id,
747                    "name": "device-keyset",
748                    "description": "Keyset with device client",
749                    "is_disabled": false,
750                    "is_default": false,
751                    "client": {
752                        "id": client_id,
753                        "client_key": "AQIDBA==",
754                    },
755                })
756            );
757
758            // Should round-trip
759            let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
760            assert_eq!(deserialized.keyset.id, keyset_id);
761            let created_client = deserialized.client.unwrap();
762            assert_eq!(created_client.id, client_id);
763            assert_eq!(&*created_client.client_key, &[1, 2, 3, 4]);
764        }
765    }
766
767    mod create_client_request_serialization {
768        use super::*;
769        use crate::CreateClientRequest;
770
771        #[test]
772        fn with_keyset_id_round_trips() {
773            let keyset_id = Uuid::new_v4();
774            let req = CreateClientRequest {
775                keyset_id: Some(IdentifiedBy::Uuid(keyset_id)),
776                name: "my-client".into(),
777                description: "desc".into(),
778            };
779
780            let serialized = serde_json::to_value(&req).unwrap();
781            assert!(serialized.get("keyset_id").is_some());
782
783            let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
784            assert_eq!(deserialized.keyset_id, Some(IdentifiedBy::Uuid(keyset_id)));
785        }
786
787        #[test]
788        fn without_keyset_id_round_trips() {
789            let req = CreateClientRequest {
790                keyset_id: None,
791                name: "my-client".into(),
792                description: "desc".into(),
793            };
794
795            let serialized = serde_json::to_value(&req).unwrap();
796            assert!(serialized.get("keyset_id").is_none());
797
798            let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
799            assert_eq!(deserialized.keyset_id, None);
800        }
801
802        #[test]
803        fn backwards_compatible_with_dataset_id() {
804            let dataset_id = Uuid::new_v4();
805            let json = json!({
806                "dataset_id": dataset_id,
807                "name": "old-client",
808                "description": "old desc",
809            });
810
811            let req: CreateClientRequest = serde_json::from_value(json).unwrap();
812            assert_eq!(req.keyset_id, Some(IdentifiedBy::Uuid(dataset_id)));
813        }
814
815        #[test]
816        fn omitted_keyset_id_defaults_to_none() {
817            let json = json!({
818                "name": "no-keyset",
819                "description": "no keyset",
820            });
821
822            let req: CreateClientRequest = serde_json::from_value(json).unwrap();
823            assert_eq!(req.keyset_id, None);
824        }
825    }
826
827    mod backwards_compatible_deserialisation {
828        use super::*;
829
830        #[test]
831        fn when_dataset_id_is_uuid() {
832            let client_id = Uuid::new_v4();
833            let dataset_id = Uuid::new_v4();
834
835            let json = json!({
836                "client_id": client_id,
837                "dataset_id": dataset_id,
838            });
839
840            let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
841
842            assert_eq!(
843                req,
844                LoadKeysetRequest {
845                    client_id,
846                    keyset_id: Some(IdentifiedBy::Uuid(dataset_id))
847                }
848            );
849        }
850
851        #[test]
852        fn when_keyset_id_is_uuid() {
853            let client_id = Uuid::new_v4();
854            let keyset_id = Uuid::new_v4();
855
856            let json = json!({
857                "client_id": client_id,
858                "keyset_id": keyset_id,
859            });
860
861            let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
862
863            assert_eq!(
864                req,
865                LoadKeysetRequest {
866                    client_id,
867                    keyset_id: Some(IdentifiedBy::Uuid(keyset_id))
868                }
869            );
870        }
871
872        #[test]
873        fn when_dataset_id_is_id_name() {
874            let client_id = Uuid::new_v4();
875            let dataset_id = IdentifiedBy::Name(Name::new_untrusted("some-dataset-name"));
876
877            let json = json!({
878                "client_id": client_id,
879                "dataset_id": dataset_id,
880            });
881
882            let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
883
884            assert_eq!(
885                req,
886                LoadKeysetRequest {
887                    client_id,
888                    keyset_id: Some(dataset_id)
889                }
890            );
891        }
892    }
893}