mod builder;
mod errors;
pub mod key_provider;
mod local_log;
mod secret_key;
mod vitur_client;
use crate::zerokms::vitur_client::{connection::HttpConnectionOpts, ClientOpts};
use local_log::log_decryptions;
use log::debug;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use url::Url;
use uuid::Uuid;
use vitur_client::{DecryptionTarget, EncryptionTarget};
use zeroize::{Zeroize, ZeroizeOnDrop};
use zerokms_protocol::{IdentifiedBy, UnverifiedContext};
use stack_auth::AuthStrategy;
use crate::credentials::ServiceToken;
pub use builder::{WithKeyProvider, ZeroKMSBuilder, ZeroKMSBuilderError};
pub use errors::Error;
pub use key_provider::{
EnvKeyProvider, FallbackKeyProvider, KeyProvider, KeyProviderError, StaticKeyProvider,
};
pub use secret_key::SecretKey;
pub use vitur_client::{
encrypted_record,
encrypted_record::{Decryptable, EncryptedRecord, WithContext},
errors::RecordDecryptError,
ClientKey, DataKey, DataKeyWithTag, EncryptPayload, GenerateKeyPayload, RetrieveKeyPayload,
};
pub use zerokms_protocol::cipherstash_config::{DatasetConfig, DatasetConfigWithIndexRootKey};
pub use zerokms_protocol::{
ClientKeysetId, ClientType, Context, CreateClientResponse, CreateClientSpec,
CreateKeysetResponse, CreatedClient, DeleteClientResponse, Keyset, KeysetClient,
ViturKeyMaterial,
};
#[doc(hidden)]
pub use vitur_client::IndexKey;
type ViturClient = vitur_client::Client<vitur_client::HttpConnection>;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct ZeroKMS<C, ClientKeyState = ()>
where
ClientKeyState: Zeroize,
{
#[zeroize(skip)]
client: ViturClient,
#[zeroize(skip)]
credentials: C,
#[zeroize(skip)]
decryption_log_path: Option<PathBuf>,
client_key: ClientKeyState,
}
pub type ZeroKMSWithClientKey<C> = ZeroKMS<C, ClientKey>;
pub struct ZeroKMSClientOpts<C> {
credentials: C,
connection_opts: HttpConnectionOpts,
max_keys_per_req: usize,
max_concurrent_reqs: usize,
}
impl<C> ZeroKMS<C>
where
C: Send + Sync + 'static,
for<'a> &'a C: AuthStrategy,
{
pub fn connect(opts: ZeroKMSClientOpts<C>) -> Result<Self, Error> {
let client_opts: ClientOpts<HttpConnectionOpts> = ClientOpts {
max_keys_per_req: opts.max_keys_per_req,
max_concurrent_reqs: opts.max_concurrent_reqs,
connection_opts: opts.connection_opts,
};
let client = ViturClient::init_opts(client_opts)?;
Ok(Self {
client,
credentials: opts.credentials,
decryption_log_path: None,
client_key: (),
})
}
pub fn connect_with_client_key(
opts: ZeroKMSClientOpts<C>,
client_key: ClientKey,
) -> Result<ZeroKMSWithClientKey<C>, Error> {
let client_opts: ClientOpts<HttpConnectionOpts> = ClientOpts {
max_keys_per_req: opts.max_keys_per_req,
max_concurrent_reqs: opts.max_concurrent_reqs,
connection_opts: opts.connection_opts,
};
let client = ViturClient::init_opts(client_opts)?;
Ok(ZeroKMSWithClientKey {
client,
credentials: opts.credentials,
decryption_log_path: None,
client_key,
})
}
#[deprecated(note = "replaced by connect", since = "0.32.2")]
pub fn new(base_url: &Url, credentials: C, decryption_log_path: Option<&Path>) -> Self {
let mut host = base_url.to_string();
if host.ends_with('/') {
host.pop();
}
#[allow(deprecated)]
let client = ViturClient::init(host);
Self {
client,
credentials,
decryption_log_path: decryption_log_path.map(|p| p.to_path_buf()),
client_key: (),
}
}
#[deprecated(note = "replaced by connect_with_client_key", since = "0.32.2")]
pub fn new_with_client_key(
base_url: &Url,
credentials: C,
decryption_log_path: Option<&Path>,
client_key: ClientKey,
) -> ZeroKMSWithClientKey<C> {
let mut host = base_url.to_string();
if host.ends_with('/') {
host.pop();
}
#[allow(deprecated)]
let client = ViturClient::init(host);
ZeroKMSWithClientKey {
client,
credentials,
decryption_log_path: decryption_log_path.map(|p| p.to_path_buf()),
client_key,
}
}
}
impl<C, K> ZeroKMS<C, K>
where
C: Send + Sync + 'static,
for<'a> &'a C: AuthStrategy,
K: Zeroize,
{
async fn get_token(&self) -> Result<stack_auth::ServiceToken, Error> {
let token = (&self.credentials).get_token().await?;
if !self.client.connection().has_base_url() {
let url = token.zerokms_url()?;
self.client.connection().ensure_base_url(url);
}
Ok(token)
}
#[deprecated(note = "removed in favor of server-side auditing", since = "0.32.2")]
pub fn log_decryptions<P>(&self, records: &[P], access_token: &str)
where
P: Decryptable,
{
if let Some(log_path) = &self.decryption_log_path {
_ = log_decryptions(records, access_token, log_path);
}
}
#[deprecated(note = "replaced by create_keyset", since = "0.26.0")]
pub async fn create_dataset(&self, name: &str, description: &str) -> Result<Keyset, Error> {
self.create_keyset(name, description).await
}
pub async fn create_keyset(&self, name: &str, description: &str) -> Result<Keyset, Error> {
let token = self.get_token().await?;
self.client
.create_keyset(name, description, token.as_str())
.await
.map_err(Error::from)
}
pub async fn create_keyset_with_client(
&self,
name: &str,
description: &str,
client_spec: CreateClientSpec<'_>,
) -> Result<CreateKeysetResponse, Error> {
let token = self.get_token().await?;
self.client
.create_keyset_with_client(name, description, client_spec, token.as_str())
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by grant_keyset", since = "0.26.0")]
pub async fn grant_dataset(&self, client_id: Uuid, keyset_id: Uuid) -> Result<(), Error> {
self.grant_keyset(client_id, keyset_id).await
}
pub async fn grant_keyset(&self, client_id: Uuid, keyset_id: Uuid) -> Result<(), Error> {
let token = self.get_token().await?;
self.client
.grant_keyset(client_id, keyset_id, token.as_str())
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by revoke_keyset", since = "0.26.0")]
pub async fn revoke_dataset(&self, client_id: Uuid, keyset_id: Uuid) -> Result<(), Error> {
self.revoke_keyset(client_id, keyset_id).await
}
pub async fn revoke_keyset(&self, client_id: Uuid, keyset_id: Uuid) -> Result<(), Error> {
let token = self.get_token().await?;
self.client
.revoke_keyset(client_id, keyset_id, token.as_str())
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by list_datasets", since = "0.26.0")]
pub async fn list_datasets(&self, include_disabled: bool) -> Result<Vec<Keyset>, Error> {
self.list_keysets(include_disabled).await
}
pub async fn list_keysets(&self, include_disabled: bool) -> Result<Vec<Keyset>, Error> {
let token = self.get_token().await?;
self.client
.list_keysets(token.as_str(), include_disabled)
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by enable_keyset", since = "0.26.0")]
pub async fn enable_dataset(&self, keyset_id: Uuid) -> Result<(), Error> {
self.enable_keyset(keyset_id).await
}
pub async fn enable_keyset(&self, keyset_id: Uuid) -> Result<(), Error> {
let token = self.get_token().await?;
self.client
.enable_keyset(keyset_id, token.as_str())
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by disable_keyset", since = "0.26.0")]
pub async fn disable_dataset(&self, keyset_id: Uuid) -> Result<(), Error> {
self.disable_keyset(keyset_id).await
}
pub async fn disable_keyset(&self, keyset_id: Uuid) -> Result<(), Error> {
let token = self.get_token().await?;
self.client
.disable_keyset(keyset_id, token.as_str())
.await
.map_err(Error::from)
}
#[deprecated(note = "replaced by modify_keyset", since = "0.26.0")]
pub async fn modify_dataset(
&self,
keyset_id: Uuid,
name: Option<&str>,
description: Option<&str>,
) -> Result<(), Error> {
self.modify_keyset(keyset_id, name, description).await
}
pub async fn modify_keyset(
&self,
keyset_id: Uuid,
name: Option<&str>,
description: Option<&str>,
) -> Result<(), Error> {
let token = self.get_token().await?;
self.client
.modify_keyset(keyset_id, name, description, token.as_str())
.await
.map_err(Error::from)
}
pub async fn create_client(
&self,
name: &str,
description: &str,
keyset_id: Option<Uuid>,
) -> Result<CreateClientResponse, Error> {
let token = self.get_token().await?;
self.client
.create_client(name, description, keyset_id, token.as_str())
.await
.map_err(Error::from)
}
pub async fn list_clients(&self) -> Result<Vec<KeysetClient>, Error> {
let token = self.get_token().await?;
self.client
.list_clients(token.as_str())
.await
.map_err(Error::from)
}
pub async fn delete_client(&self, client_id: Uuid) -> Result<DeleteClientResponse, Error> {
let token = self.get_token().await?;
self.client
.delete_client(client_id, token.as_str())
.await
.map_err(Error::from)
}
}
impl<C> ZeroKMSWithClientKey<C>
where
C: Send + Sync + 'static,
for<'a> &'a C: AuthStrategy,
{
pub async fn encrypt(
&self,
payloads: impl IntoIterator<Item = EncryptPayload<'_>>,
keyset_id: Option<Uuid>,
) -> Result<Vec<EncryptedRecord>, Error> {
debug!(target: "zero_kms::encrypt", "encrypting records");
let payloads: Vec<_> = payloads.into_iter().collect();
if payloads.is_empty() {
debug!(target: "zero_kms::encrypt", "no records to encrypt");
return Ok(vec![]);
}
debug!(target: "zero_kms::encrypt", "waiting for access token");
let token = self.get_token().await?;
debug!(target: "zero_kms::encrypt", "got token, encrypting");
let res = self
.client
.encrypt(payloads, &self.client_key, keyset_id, token.as_str())
.await?;
debug!(target: "zero_kms::encrypt", "success, encrypted {} records", res.len());
Ok(res)
}
pub async fn encrypt_single(
&self,
payload: EncryptPayload<'_>,
keyset_id: Option<Uuid>,
) -> Result<EncryptedRecord, Error> {
debug!(target: "zero_kms::encrypt_single", "encrypting record - waiting for access token");
let token = self.get_token().await?;
debug!(target: "zero_kms::encrypt_single", "got token, encrypting");
let res = self
.client
.encrypt_single(payload, &self.client_key, keyset_id, token.as_str())
.await?;
debug!(target: "zero_kms::encrypt_single", "success");
Ok(res)
}
pub async fn decrypt<'a, P>(
&self,
payloads: impl IntoIterator<Item = P>,
keyset_id: Option<Uuid>,
service_token: Option<Cow<'a, ServiceToken>>,
unverified_context: Option<&'a UnverifiedContext>, ) -> Result<Vec<Vec<u8>>, Error>
where
P: Decryptable,
{
debug!(target: "zero_kms::decrypt", "decrypting records");
let payloads: Vec<_> = payloads.into_iter().collect();
if payloads.is_empty() {
debug!(target: "zero_kms::decrypt", "no records to decrypt");
return Ok(vec![]);
}
let token = self.resolve_token(service_token).await?;
debug!(target: "zero_kms::decrypt", "got token, decrypting {} records", payloads.len());
let res = self
.client
.decrypt(
payloads,
&self.client_key,
keyset_id,
&token,
unverified_context,
)
.await?;
debug!(target: "zero_kms::decrypt", "success, decrypted {} records", res.len());
Ok(res)
}
pub async fn decrypt_fallible<'a, P>(
&self,
payloads: impl IntoIterator<Item = P>,
service_token: Option<Cow<'a, ServiceToken>>,
unverified_context: Option<Cow<'a, UnverifiedContext>>,
) -> Result<Vec<Result<Vec<u8>, RecordDecryptError>>, Error>
where
P: Decryptable,
{
debug!(target: "zero_kms::decrypt", "decrypting records");
let payloads: Vec<_> = payloads.into_iter().collect();
if payloads.is_empty() {
debug!(target: "zero_kms::decrypt", "no records to decrypt");
return Ok(vec![]);
}
let token = self.resolve_token(service_token).await?;
debug!(target: "zero_kms::decrypt", "got token, decrypting {} records", payloads.len());
let unverified_context = unverified_context.as_deref().map(Cow::Borrowed);
let res = self
.client
.decrypt_fallible(payloads, &self.client_key, &token, unverified_context)
.await?;
debug!(target: "zero_kms::decrypt", "success, decrypted {} records", res.len());
Ok(res)
}
pub async fn decrypt_single<'a, P>(
&self,
payload: P,
keyset_id: Option<Uuid>,
service_token: Option<Cow<'a, ServiceToken>>,
unverified_context: Option<&UnverifiedContext>,
) -> Result<Vec<u8>, Error>
where
P: Decryptable,
{
let token = self.resolve_token(service_token).await?;
debug!(target: "zero_kms::decrypt_single", "got token, decrypting record");
let res = self
.client
.decrypt_single(
payload,
&self.client_key,
keyset_id,
&token,
unverified_context,
)
.await?;
debug!(target: "zero_kms::decrypt_single", "success");
Ok(res)
}
pub(crate) async fn generate_data_keys<'a>(
&self,
payloads: impl IntoIterator<Item = GenerateKeyPayload<'_>>,
keyset_id: Option<Uuid>,
service_token: Option<Cow<'a, ServiceToken>>,
unverified_context: Option<Cow<'a, UnverifiedContext>>,
) -> Result<Vec<DataKeyWithTag>, Error> {
let token = self.resolve_token(service_token).await?;
self.client
.generate_keys(
payloads,
&self.client_key,
keyset_id,
&token,
unverified_context,
)
.await
.map_err(Error::from)
}
pub(crate) async fn load_keyset_index_key(
&self,
keyset_id: Option<IdentifiedBy>,
) -> Result<(Uuid, IndexKey), Error> {
let token = self.get_token().await?;
let (keyset, index_key) = self
.client
.load_keyset(&self.client_key, keyset_id, token.as_str())
.await
.map_err(Error::from)?;
debug!(target: "zero_kms::load_keyset", "loaded keyset: [{}]({})", keyset.id, keyset.name);
Ok((keyset.id, index_key))
}
async fn resolve_token<'a>(
&self,
service_token: Option<Cow<'a, ServiceToken>>,
) -> Result<String, Error> {
if let Some(st) = service_token {
debug!(target: "zero_kms::resolve_token", "using service token from args");
let raw = st.token().to_owned();
if !self.client.connection().has_base_url() {
let parsed = stack_auth::ServiceToken::new(stack_auth::SecretToken::new(&raw));
let url = parsed.zerokms_url()?;
self.client.connection().ensure_base_url(url);
}
Ok(raw)
} else {
debug!(target: "zero_kms::resolve_token", "getting token from credentials");
let token = self.get_token().await?;
Ok(token.as_str().to_owned())
}
}
}
#[inline]
pub fn encrypt<'p, T>(
plaintext: T,
key: DataKeyWithTag,
keyset_id: Option<Uuid>,
) -> Result<EncryptedRecord, Error>
where
EncryptPayload<'p>: From<T>,
{
let target = EncryptionTarget::new(EncryptPayload::from(plaintext), key, keyset_id);
vitur_client::encrypt(target).map_err(Error::from)
}
#[inline]
pub fn decrypt<D>(encrypted: D, key: DataKey) -> Result<Vec<u8>, RecordDecryptError>
where
D: Decryptable,
{
let target = encrypted
.into_encrypted_record()
.map_err(|e| Error::Unexpected(e.to_string()))
.map(|record| DecryptionTarget::new(record, key))
.map_err(|e| RecordDecryptError {
reason: e.to_string(),
})?;
vitur_client::decrypt(target)
}