Skip to main content

ironcore_alloy/
deterministic.rs

1use crate::{
2    AlloyMetadata, DerivationPath, EncryptedBytes, FieldId, PlaintextBytes, Secret, SecretPath,
3    TenantId, create_batch_result_struct, create_batch_result_struct_using_newtype,
4    errors::AlloyError, util,
5};
6use aes_gcm::KeyInit;
7use aes_siv::siv::Aes256Siv;
8use bytes::Bytes;
9use ironcore_documents::{impl_secret_debug, v5::key_id_header::KeyIdHeader};
10use std::collections::HashMap;
11use uniffi::custom_newtype;
12
13#[derive(Debug, Clone, uniffi::Record)]
14pub struct EncryptedField {
15    pub encrypted_field: EncryptedBytes,
16    pub secret_path: SecretPath,
17    pub derivation_path: DerivationPath,
18}
19
20#[derive(Debug, Clone, uniffi::Record)]
21pub struct PlaintextField {
22    pub plaintext_field: PlaintextBytes,
23    pub secret_path: SecretPath,
24    pub derivation_path: DerivationPath,
25}
26#[derive(Debug, Clone)]
27pub struct PlaintextFields(pub HashMap<FieldId, PlaintextField>);
28custom_newtype!(PlaintextFields, HashMap<FieldId, PlaintextField>);
29#[derive(Debug, Clone)]
30pub struct EncryptedFields(pub HashMap<FieldId, EncryptedField>);
31custom_newtype!(EncryptedFields, HashMap<FieldId, EncryptedField>);
32pub struct GenerateFieldQueryResult(pub HashMap<FieldId, Vec<EncryptedField>>);
33custom_newtype!(GenerateFieldQueryResult, HashMap<FieldId, Vec<EncryptedField>>);
34create_batch_result_struct!(DeterministicRotateResult, EncryptedField, FieldId);
35create_batch_result_struct_using_newtype!(
36    DeterministicEncryptBatchResult,
37    EncryptedField,
38    FieldId,
39    EncryptedFields
40);
41create_batch_result_struct_using_newtype!(
42    DeterministicDecryptBatchResult,
43    PlaintextField,
44    FieldId,
45    PlaintextFields
46);
47
48/// Key used for deterministic operations.
49#[derive(Clone)]
50pub(crate) struct DeterministicEncryptionKey(pub Vec<u8>);
51impl_secret_debug!(DeterministicEncryptionKey);
52
53impl DeterministicEncryptionKey {
54    /// A way to generate a key from the secret, tenant_id and derivation_path. This is done in the context of
55    /// a standalone secret where we don't have a TSP to call to for derivation.
56    pub(crate) fn derive_from_secret(
57        secret: &Secret,
58        tenant_id: &TenantId,
59        derivation_path: &DerivationPath,
60    ) -> Self {
61        let hash_result = util::hash512(
62            &secret.secret[..],
63            format!("{}-{}", tenant_id.0, derivation_path.0),
64        );
65        Self(hash_result.to_vec())
66    }
67}
68
69#[uniffi::export]
70#[async_trait::async_trait]
71pub trait DeterministicFieldOps: Send + Sync {
72    /// Encrypt a field with the provided metadata.
73    /// Because the field is encrypted deterministically with each call, the result will be the same for repeated calls.
74    /// This allows for exact matches and indexing of the encrypted field, but comes with some security considerations.
75    /// If you don't need to support these use cases, we recommend using `standard` encryption instead.
76    async fn encrypt(
77        &self,
78        plaintext_field: PlaintextField,
79        metadata: &AlloyMetadata,
80    ) -> Result<EncryptedField, AlloyError>;
81    /// Deterministically encrypt the provided fields with the provided metadata.
82    /// Because the fields are encrypted deterministically with each call, the result will be the same for repeated calls.
83    /// This allows for exact matches and indexing of the encrypted field, but comes with some security considerations.
84    /// If you don't need to support these use cases, we recommend using `standard` encryption instead.
85    async fn encrypt_batch(
86        &self,
87        fields: PlaintextFields,
88        metadata: &AlloyMetadata,
89    ) -> Result<DeterministicEncryptBatchResult, AlloyError>;
90    /// Decrypt a field that was deterministically encrypted with the provided metadata.
91    async fn decrypt(
92        &self,
93        encrypted_field: EncryptedField,
94        metadata: &AlloyMetadata,
95    ) -> Result<PlaintextField, AlloyError>;
96    /// Decrypt each of the fields that were deterministically encrypted with the provided metadata.
97    /// Note that because the metadata is shared between the fields, they all must correspond to the
98    /// same tenant ID.
99    async fn decrypt_batch(
100        &self,
101        encrypted_fields: EncryptedFields,
102        metadata: &AlloyMetadata,
103    ) -> Result<DeterministicDecryptBatchResult, AlloyError>;
104    /// Encrypt each plaintext field with any Current and InRotation keys for the provided secret path.
105    /// The resulting encrypted fields should be used in tandem when querying the data store.
106    async fn generate_query_field_values(
107        &self,
108        fields_to_query: PlaintextFields,
109        metadata: &AlloyMetadata,
110    ) -> Result<GenerateFieldQueryResult, AlloyError>;
111    /// Re-encrypt already encrypted fields with the Current key for the provided tenant. The `metadata` passed
112    /// must contain the tenant ID that the fields were originally encrypted to. If `new_tenant_id` is empty,
113    /// the fields will simply be encrypted with the same tenant's current secret.
114    async fn rotate_fields(
115        &self,
116        encrypted_fields: EncryptedFields,
117        metadata: &AlloyMetadata,
118        new_tenant_id: Option<TenantId>,
119    ) -> Result<DeterministicRotateResult, AlloyError>;
120    /// Generate a prefix that could used to search a data store for fields encrypted using an identifier (KMS
121    /// config id for SaaS Shield, secret id for Standalone). These bytes should be encoded into
122    /// a format matching the encoding in the data store. z85/ascii85 users should first pass these bytes through
123    /// `encode_prefix_z85` or `base85_prefix_padding`. Make sure you've read the documentation of those functions to
124    /// avoid pitfalls when encoding across byte boundaries.
125    async fn get_in_rotation_prefix(
126        &self,
127        secret_path: SecretPath,
128        derivation_path: DerivationPath,
129        metadata: &AlloyMetadata,
130    ) -> Result<Vec<u8>, AlloyError>;
131}
132
133pub(crate) fn encrypt_internal(
134    key: DeterministicEncryptionKey,
135    key_id_header: KeyIdHeader,
136    plaintext_field: PlaintextField,
137) -> Result<EncryptedField, AlloyError> {
138    let current_derived_key_sized: [u8; 64] =
139        key.0.try_into().map_err(|_| AlloyError::InvalidKey {
140            msg: "The derived key was not 64 bytes.".to_string(),
141        })?;
142    let encrypted_bytes = deterministic_encrypt(
143        current_derived_key_sized,
144        plaintext_field.plaintext_field.0.as_slice(),
145    )?;
146    let encrypted_field = key_id_header.put_header_on_document(encrypted_bytes);
147    Ok(EncryptedField {
148        encrypted_field: EncryptedBytes(encrypted_field.into()),
149        secret_path: plaintext_field.secret_path,
150        derivation_path: plaintext_field.derivation_path,
151    })
152}
153
154pub(crate) fn decrypt_internal(
155    key: DeterministicEncryptionKey,
156    ciphertext: Bytes,
157    secret_path: SecretPath,
158    derivation_path: DerivationPath,
159) -> Result<PlaintextField, AlloyError> {
160    let sized_key: [u8; 64] = key.0.try_into().map_err(|_| AlloyError::InvalidKey {
161        msg: "The derived key was not 64 bytes.".to_string(),
162    })?;
163    deterministic_decrypt(sized_key, &ciphertext).map(|res| PlaintextField {
164        plaintext_field: res,
165        secret_path,
166        derivation_path,
167    })
168}
169
170fn deterministic_encrypt(key: [u8; 64], plaintext: &[u8]) -> Result<Vec<u8>, AlloyError> {
171    deterministic_encrypt_core(key, plaintext, &[])
172}
173
174fn deterministic_encrypt_core(
175    key: [u8; 64],
176    plaintext: &[u8],
177    associated_data: &[u8],
178) -> Result<Vec<u8>, AlloyError> {
179    let mut cipher = Aes256Siv::new(&key.into());
180    cipher
181        .encrypt([associated_data], plaintext)
182        .map_err(|e| AlloyError::EncryptError { msg: e.to_string() })
183}
184
185fn deterministic_decrypt(key: [u8; 64], ciphertext: &[u8]) -> Result<PlaintextBytes, AlloyError> {
186    deterministic_decrypt_core(key, ciphertext, &[])
187}
188
189fn deterministic_decrypt_core(
190    key: [u8; 64],
191    ciphertext: &[u8],
192    associated_data: &[u8],
193) -> Result<PlaintextBytes, AlloyError> {
194    let mut cipher = Aes256Siv::new(&key.into());
195    cipher
196        .decrypt([associated_data], ciphertext)
197        .map(PlaintextBytes)
198        .map_err(|_| AlloyError::DecryptError {
199            msg: "Failed deterministic decryption. Ensure the data and tenant ID are correct"
200                .to_string(),
201        })
202}
203
204#[cfg(test)]
205mod test {
206    use super::*;
207    use hex_literal::hex;
208
209    // Our TSCs test the example from https://datatracker.ietf.org/doc/html/rfc5297#appendix-A.1, but that uses Aes128 (our TSC
210    // crypto dependencies are more flexible than here). So this example comes from https://github.com/RustCrypto/AEADs/blob/master/aes-siv/tests/siv.rs#L160,
211    // but that may not prove much since that is this own library's test.
212    #[test]
213    fn test_known_deterministic() {
214        let key = hex!(
215            "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f06f6e6d6c6b6a69686766656463626160f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f"
216        );
217        let ad = hex!("101112131415161718191a1b1c1d1e1f2021222324252627");
218        let plaintext = hex!("112233445566778899aabbccddee");
219        let encrypt_result = deterministic_encrypt_core(key, &plaintext, &ad).unwrap();
220        let expected_encrypt = hex!("f125274c598065cfc26b0e71575029088b035217e380cac8919ee800c126");
221        assert_eq!(encrypt_result.clone(), expected_encrypt);
222        let decrypt_result = deterministic_decrypt_core(key, &encrypt_result, &ad).unwrap();
223        assert_eq!(decrypt_result.0, plaintext);
224    }
225}