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