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