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