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