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