use core::fmt;
use std::collections::BTreeMap;
use reqwest::{Method, StatusCode};
use secrecy::{ExposeSecret, SecretString};
use serde::{
Deserialize, Deserializer, Serialize,
de::{Error as DeError, IgnoredAny, MapAccess, Visitor},
};
#[cfg(feature = "transit-import")]
use zeroize::Zeroize;
#[cfg(any(feature = "transit-bytes", feature = "transit-import"))]
use zeroize::Zeroizing;
use crate::{
Authenticated, Client, Error, Result,
path::{validate_endpoint_path, validate_mount_path},
response::{
Empty, ListEntries, ListPageOptions, ResponseEnvelope, deserialize_bounded_string_vec,
},
};
#[cfg(feature = "transit-import")]
const TRANSIT_WRAPPING_KEY_BYTES: usize = 512;
#[cfg(feature = "transit-import")]
const TRANSIT_IMPORT_EPHEMERAL_AES_KEY_BYTES: usize = 32;
#[cfg(feature = "transit-import")]
const MAX_TRANSIT_IMPORT_KEY_MATERIAL_BYTES: usize = 16 * 1024;
pub const MAX_TRANSIT_BATCH_ITEMS: usize = 1000;
#[derive(Debug)]
pub struct Transit<'a> {
client: &'a Client<Authenticated>,
mount: Vec<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum TransitKeyType {
#[serde(rename = "aes128-gcm96")]
Aes128Gcm96,
#[serde(rename = "aes256-gcm96")]
Aes256Gcm96,
#[serde(rename = "chacha20-poly1305")]
ChaCha20Poly1305,
#[serde(rename = "xchacha20-poly1305")]
XChaCha20Poly1305,
#[serde(rename = "ed25519")]
Ed25519,
#[serde(rename = "ecdsa-p256")]
EcdsaP256,
#[serde(rename = "ecdsa-p384")]
EcdsaP384,
#[serde(rename = "ecdsa-p521")]
EcdsaP521,
#[serde(rename = "rsa-2048")]
Rsa2048,
#[serde(rename = "rsa-3072")]
Rsa3072,
#[serde(rename = "rsa-4096")]
Rsa4096,
#[serde(rename = "hmac")]
Hmac,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitDataKeyType {
Plaintext,
Wrapped,
}
impl TransitDataKeyType {
fn as_path_segment(self) -> &'static str {
match self {
Self::Plaintext => "plaintext",
Self::Wrapped => "wrapped",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitRandomSource {
Platform,
All,
}
impl TransitRandomSource {
fn as_path_segment(self) -> &'static str {
match self {
Self::Platform => "platform",
Self::All => "all",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitHashAlgorithm {
#[cfg(feature = "allow-sha1-acknowledged")]
#[deprecated(since = "0.3.0", note = "SHA-1 is broken; use SHA2-256 or stronger")]
Sha1,
Sha2_224,
Sha2_256,
Sha2_384,
Sha2_512,
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
None,
}
impl TransitHashAlgorithm {
fn as_path_segment(self) -> &'static str {
match self {
#[cfg(feature = "allow-sha1-acknowledged")]
#[allow(deprecated)]
Self::Sha1 => "sha1",
Self::Sha2_224 => "sha2-224",
Self::Sha2_256 => "sha2-256",
Self::Sha2_384 => "sha2-384",
Self::Sha2_512 => "sha2-512",
Self::Sha3_224 => "sha3-224",
Self::Sha3_256 => "sha3-256",
Self::Sha3_384 => "sha3-384",
Self::Sha3_512 => "sha3-512",
Self::None => "none",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum TransitOutputFormat {
#[serde(rename = "hex")]
Hex,
#[serde(rename = "base64")]
Base64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum TransitSignatureAlgorithm {
#[serde(rename = "pss")]
Pss,
#[serde(rename = "pkcs1v15")]
Pkcs1v15,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum TransitMarshalingAlgorithm {
#[serde(rename = "asn1")]
Asn1,
#[serde(rename = "jws")]
Jws,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitSaltLength {
Auto,
Hash,
Value(u64),
}
impl Serialize for TransitSaltLength {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Auto => serializer.serialize_str("auto"),
Self::Hash => serializer.serialize_str("hash"),
Self::Value(value) => serializer.serialize_u64(*value),
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TransitCreateKeyRequest {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub key_type: Option<TransitKeyType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub convergent_encryption: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub derived: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exportable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_plaintext_backup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_rotate_period: Option<String>,
}
impl TransitCreateKeyRequest {
fn validate(&self) -> Result<()> {
if let Some(period) = &self.auto_rotate_period {
crate::validation::validate_duration_parameter(period, "Transit auto_rotate_period")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitKeyInfo {
#[serde(default)]
pub name: Option<String>,
#[serde(rename = "type")]
pub key_type: String,
#[serde(default)]
pub deletion_allowed: bool,
#[serde(default)]
pub derived: bool,
#[serde(default)]
pub exportable: bool,
#[serde(default)]
pub allow_plaintext_backup: bool,
#[serde(default, deserialize_with = "deserialize_bounded_u64_map")]
pub keys: BTreeMap<String, u64>,
#[serde(default)]
pub min_decryption_version: u64,
#[serde(default)]
pub min_encryption_version: u64,
#[serde(default)]
pub supports_encryption: bool,
#[serde(default)]
pub supports_decryption: bool,
#[serde(default)]
pub supports_derivation: bool,
#[serde(default)]
pub supports_signing: bool,
#[serde(default)]
pub imported: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TransitUpdateKeyRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub min_decryption_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_encryption_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deletion_allowed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exportable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_plaintext_backup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_rotate_period: Option<String>,
}
impl TransitUpdateKeyRequest {
#[must_use]
pub fn allow_deletion(mut self) -> Self {
self.deletion_allowed = Some(true);
self
}
pub fn with_auto_rotate_period(mut self, period: impl Into<String>) -> Result<Self> {
let period = period.into();
crate::validation::validate_duration_parameter(&period, "Transit auto_rotate_period")?;
self.auto_rotate_period = Some(period);
Ok(self)
}
fn validate(&self) -> Result<()> {
if let Some(period) = &self.auto_rotate_period {
crate::validation::validate_duration_parameter(period, "Transit auto_rotate_period")?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum TransitImportHashFunction {
#[cfg(feature = "allow-sha1-acknowledged")]
#[serde(rename = "SHA1")]
#[deprecated(since = "0.11.0", note = "SHA-1 is broken; use SHA256 or stronger")]
Sha1,
#[serde(rename = "SHA224")]
Sha224,
#[serde(rename = "SHA256")]
Sha256,
#[serde(rename = "SHA384")]
Sha384,
#[serde(rename = "SHA512")]
Sha512,
}
impl TransitImportHashFunction {
fn as_query_value(self) -> &'static str {
match self {
#[cfg(feature = "allow-sha1-acknowledged")]
#[allow(deprecated)]
Self::Sha1 => "SHA1",
Self::Sha224 => "SHA224",
Self::Sha256 => "SHA256",
Self::Sha384 => "SHA384",
Self::Sha512 => "SHA512",
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitWrappingKey {
pub public_key: String,
}
#[derive(Clone)]
pub struct TransitImportRequest {
pub ciphertext: Option<SecretString>,
pub public_key: Option<String>,
pub key_type: TransitKeyType,
pub hash_function: Option<TransitImportHashFunction>,
pub allow_rotation: Option<bool>,
pub derived: Option<bool>,
pub context: Option<SecretString>,
pub exportable: Option<bool>,
pub allow_plaintext_backup: Option<bool>,
pub auto_rotate_period: Option<String>,
}
impl fmt::Debug for TransitImportRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitImportRequest")
.field(
"ciphertext",
&self.ciphertext.as_ref().map(|_| "<redacted>"),
)
.field("public_key", &self.public_key)
.field("key_type", &self.key_type)
.field("hash_function", &self.hash_function)
.field("allow_rotation", &self.allow_rotation)
.field("derived", &self.derived)
.field("context", &self.context.as_ref().map(|_| "<redacted>"))
.field("exportable", &self.exportable)
.field("allow_plaintext_backup", &self.allow_plaintext_backup)
.field("auto_rotate_period", &self.auto_rotate_period)
.finish()
}
}
impl TransitImportRequest {
pub fn new(ciphertext: SecretString, key_type: TransitKeyType) -> Result<Self> {
validate_non_empty_secret(&ciphertext, "Transit import ciphertext")?;
Ok(Self {
ciphertext: Some(ciphertext),
public_key: None,
key_type,
hash_function: None,
allow_rotation: None,
derived: None,
context: None,
exportable: None,
allow_plaintext_backup: None,
auto_rotate_period: None,
})
}
pub fn from_public_key(
public_key: impl Into<String>,
key_type: TransitKeyType,
) -> Result<Self> {
let public_key = public_key.into();
validate_non_empty_public_key(&public_key, "Transit import public_key")?;
Ok(Self {
ciphertext: None,
public_key: Some(public_key),
key_type,
hash_function: None,
allow_rotation: None,
derived: None,
context: None,
exportable: None,
allow_plaintext_backup: None,
auto_rotate_period: None,
})
}
#[must_use]
pub fn with_hash_function(mut self, hash_function: TransitImportHashFunction) -> Self {
self.hash_function = Some(hash_function);
self
}
#[must_use]
pub fn allow_rotation(mut self) -> Self {
self.allow_rotation = Some(true);
self
}
#[must_use]
pub fn with_context(mut self, context: SecretString) -> Self {
self.derived = Some(true);
self.context = Some(context);
self
}
#[must_use]
pub fn exportable(mut self) -> Self {
self.exportable = Some(true);
self
}
#[must_use]
pub fn allow_plaintext_backup(mut self) -> Self {
self.allow_plaintext_backup = Some(true);
self
}
pub fn with_auto_rotate_period(mut self, period: impl Into<String>) -> Result<Self> {
let period = period.into();
crate::validation::validate_duration_parameter(&period, "Transit auto_rotate_period")?;
self.auto_rotate_period = Some(period);
Ok(self)
}
fn validate(&self) -> Result<()> {
validate_import_material(
self.ciphertext.as_ref(),
self.public_key.as_deref(),
"Transit import",
)?;
if let Some(period) = &self.auto_rotate_period {
crate::validation::validate_duration_parameter(period, "Transit auto_rotate_period")?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct TransitImportVersionRequest {
pub ciphertext: Option<SecretString>,
pub public_key: Option<String>,
pub hash_function: Option<TransitImportHashFunction>,
pub context: Option<SecretString>,
pub version: Option<u64>,
}
impl fmt::Debug for TransitImportVersionRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitImportVersionRequest")
.field(
"ciphertext",
&self.ciphertext.as_ref().map(|_| "<redacted>"),
)
.field("public_key", &self.public_key)
.field("hash_function", &self.hash_function)
.field("context", &self.context.as_ref().map(|_| "<redacted>"))
.field("version", &self.version)
.finish()
}
}
impl TransitImportVersionRequest {
pub fn new(ciphertext: SecretString) -> Result<Self> {
validate_non_empty_secret(&ciphertext, "Transit import ciphertext")?;
Ok(Self {
ciphertext: Some(ciphertext),
public_key: None,
hash_function: None,
context: None,
version: None,
})
}
pub fn from_public_key(public_key: impl Into<String>) -> Result<Self> {
let public_key = public_key.into();
validate_non_empty_public_key(&public_key, "Transit import public_key")?;
Ok(Self {
ciphertext: None,
public_key: Some(public_key),
hash_function: None,
context: None,
version: None,
})
}
#[must_use]
pub fn with_hash_function(mut self, hash_function: TransitImportHashFunction) -> Self {
self.hash_function = Some(hash_function);
self
}
#[must_use]
pub fn with_context(mut self, context: SecretString) -> Self {
self.context = Some(context);
self
}
pub fn with_version(mut self, version: u64) -> Result<Self> {
if version == 0 {
return Err(Error::InvalidParameter(
"Transit import version must be greater than zero".into(),
));
}
self.version = Some(version);
Ok(self)
}
fn validate(&self) -> Result<()> {
validate_import_material(
self.ciphertext.as_ref(),
self.public_key.as_deref(),
"Transit import",
)
}
}
#[cfg(feature = "transit-import")]
#[derive(Clone)]
#[must_use = "Transit wrapped import ciphertext is secret-bearing material; pass it to an import request or zeroize/drop it promptly"]
pub struct TransitWrappedImportKey {
pub ciphertext: SecretString,
pub hash_function: TransitImportHashFunction,
}
#[cfg(feature = "transit-import")]
impl fmt::Debug for TransitWrappedImportKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitWrappedImportKey")
.field("ciphertext", &"<redacted>")
.field("hash_function", &self.hash_function)
.finish()
}
}
#[cfg(feature = "transit-import")]
impl TransitWrappedImportKey {
#[must_use = "import the returned ciphertext promptly; software-wrapped key material remains sensitive until consumed and dropped"]
pub fn wrap_key_material(
wrapping_key_pem: &str,
key_material: Zeroizing<Vec<u8>>,
hash_function: TransitImportHashFunction,
) -> Result<Self> {
let mut rng = rand::rng();
Self::wrap_key_material_with_rng(&mut rng, wrapping_key_pem, key_material, hash_function)
}
#[must_use = "import the returned ciphertext promptly; software-wrapped key material remains sensitive until consumed and dropped"]
pub fn wrap_key_material_with_rng<R>(
rng: &mut R,
wrapping_key_pem: &str,
key_material: Zeroizing<Vec<u8>>,
hash_function: TransitImportHashFunction,
) -> Result<Self>
where
R: rand::CryptoRng + ?Sized,
{
wrap_import_key_material(rng, wrapping_key_pem, key_material, hash_function)
}
pub fn into_import_request(self, key_type: TransitKeyType) -> Result<TransitImportRequest> {
Ok(TransitImportRequest::new(self.ciphertext, key_type)?
.with_hash_function(self.hash_function))
}
pub fn into_import_version_request(self) -> Result<TransitImportVersionRequest> {
Ok(TransitImportVersionRequest::new(self.ciphertext)?
.with_hash_function(self.hash_function))
}
}
#[derive(Clone, Deserialize)]
pub struct TransitByokExport {
#[serde(default)]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_secret_map")]
pub keys: BTreeMap<String, SecretString>,
}
impl fmt::Debug for TransitByokExport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitByokExport")
.field("name", &self.name)
.field("keys", &format_args!("<{} redacted>", self.keys.len()))
.finish()
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct TransitGlobalKeyConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_upsert: Option<bool>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct TransitCacheConfig {
pub size: u64,
}
impl TransitCacheConfig {
pub fn new(size: u64) -> Result<Self> {
if size != 0 && size < 10 {
return Err(Error::InvalidParameter(
"Transit cache size must be 0 or at least 10".into(),
));
}
Ok(Self { size })
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TransitCsrRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub csr: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitCsrResponse {
pub name: String,
#[serde(rename = "type")]
pub key_type: String,
pub csr: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct TransitSetCertificateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<u64>,
pub certificate_chain: String,
}
impl TransitSetCertificateRequest {
pub fn new(certificate_chain: impl Into<String>) -> Result<Self> {
let certificate_chain = certificate_chain.into();
if certificate_chain.trim().is_empty() {
return Err(Error::InvalidParameter(
"Transit certificate chain must not be empty".into(),
));
}
Ok(Self {
version: None,
certificate_chain,
})
}
pub fn with_version(mut self, version: u64) -> Result<Self> {
if version == 0 {
return Err(Error::InvalidParameter(
"Transit certificate version must be greater than zero".into(),
));
}
self.version = Some(version);
Ok(self)
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitCertificateChain {
pub name: String,
#[serde(rename = "type")]
pub key_type: String,
pub certificate_chain: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransitExportKeyType {
EncryptionKey,
SigningKey,
HmacKey,
PublicKey,
CertificateChain,
}
impl TransitExportKeyType {
fn as_path_segment(self) -> &'static str {
match self {
Self::EncryptionKey => "encryption-key",
Self::SigningKey => "signing-key",
Self::HmacKey => "hmac-key",
Self::PublicKey => "public-key",
Self::CertificateChain => "certificate-chain",
}
}
}
#[derive(Clone, Deserialize)]
pub struct TransitExportResponse {
#[serde(default, deserialize_with = "deserialize_bounded_secret_map")]
pub keys: BTreeMap<String, SecretString>,
#[serde(default, rename = "type")]
pub key_type: Option<String>,
#[serde(default)]
pub name: Option<String>,
}
impl fmt::Debug for TransitExportResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitExportResponse")
.field("keys", &format_args!("<{} redacted>", self.keys.len()))
.field("key_type", &self.key_type)
.field("name", &self.name)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct TransitBackup {
pub backup: SecretString,
}
impl fmt::Debug for TransitBackup {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitBackup")
.field("backup", &"<redacted>")
.finish()
}
}
#[derive(Clone)]
pub struct TransitRestoreRequest {
pub backup: SecretString,
pub force: Option<bool>,
}
impl fmt::Debug for TransitRestoreRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitRestoreRequest")
.field("backup", &"<redacted>")
.field("force", &self.force)
.finish()
}
}
impl TransitRestoreRequest {
pub fn new(backup: SecretString) -> Self {
Self {
backup,
force: None,
}
}
#[must_use]
pub fn force(mut self) -> Self {
self.force = Some(true);
self
}
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct TransitTrimRequest {
pub min_available_version: u64,
}
impl TransitTrimRequest {
pub fn new(min_available_version: u64) -> Result<Self> {
if min_available_version == 0 {
return Err(Error::InvalidParameter(
"Transit min_available_version must be greater than zero".into(),
));
}
Ok(Self {
min_available_version,
})
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitKeyList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for TransitKeyList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug)]
pub struct TransitEncryptRequest {
pub plaintext: SecretString,
pub associated_data: Option<SecretString>,
pub context: Option<SecretString>,
pub key_version: Option<u64>,
pub nonce: Option<SecretString>,
}
impl TransitEncryptRequest {
pub fn new(plaintext: SecretString) -> Self {
Self {
plaintext,
associated_data: None,
context: None,
key_version: None,
nonce: None,
}
}
#[cfg(feature = "transit-bytes")]
pub fn from_plaintext_bytes(plaintext: &[u8]) -> Result<Self> {
Ok(Self::new(base64_encode_secret(plaintext)?))
}
#[cfg(feature = "transit-bytes")]
pub fn with_associated_data_bytes(mut self, associated_data: &[u8]) -> Result<Self> {
self.associated_data = Some(base64_encode_secret(associated_data)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_context_bytes(mut self, context: &[u8]) -> Result<Self> {
self.context = Some(base64_encode_secret(context)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_nonce_bytes(mut self, nonce: &[u8]) -> Result<Self> {
self.nonce = Some(base64_encode_secret(nonce)?);
Ok(self)
}
}
#[derive(Clone, Deserialize)]
pub struct TransitEncryptResponse {
pub ciphertext: SecretString,
#[serde(default)]
pub key_version: Option<u64>,
}
impl fmt::Debug for TransitEncryptResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitEncryptResponse")
.field("ciphertext", &"<redacted>")
.field("key_version", &self.key_version)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct TransitDecryptRequest {
pub ciphertext: SecretString,
pub associated_data: Option<SecretString>,
pub context: Option<SecretString>,
pub nonce: Option<SecretString>,
}
impl TransitDecryptRequest {
pub fn new(ciphertext: SecretString) -> Self {
Self {
ciphertext,
associated_data: None,
context: None,
nonce: None,
}
}
#[cfg(feature = "transit-bytes")]
pub fn with_associated_data_bytes(mut self, associated_data: &[u8]) -> Result<Self> {
self.associated_data = Some(base64_encode_secret(associated_data)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_context_bytes(mut self, context: &[u8]) -> Result<Self> {
self.context = Some(base64_encode_secret(context)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_nonce_bytes(mut self, nonce: &[u8]) -> Result<Self> {
self.nonce = Some(base64_encode_secret(nonce)?);
Ok(self)
}
}
#[derive(Clone, Deserialize)]
pub struct TransitDecryptResponse {
pub plaintext: SecretString,
}
impl fmt::Debug for TransitDecryptResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitDecryptResponse")
.field("plaintext", &"<redacted>")
.finish()
}
}
impl TransitDecryptResponse {
#[cfg(feature = "transit-bytes")]
pub fn plaintext_bytes(&self) -> Result<Zeroizing<Vec<u8>>> {
decode_base64_secret(&self.plaintext)
}
}
#[derive(Clone, Debug)]
pub struct TransitRewrapRequest {
pub ciphertext: SecretString,
pub context: Option<SecretString>,
pub key_version: Option<u64>,
pub nonce: Option<SecretString>,
}
impl TransitRewrapRequest {
pub fn new(ciphertext: SecretString) -> Self {
Self {
ciphertext,
context: None,
key_version: None,
nonce: None,
}
}
}
#[derive(Clone, Deserialize)]
pub struct TransitRewrapResponse {
pub ciphertext: SecretString,
}
impl fmt::Debug for TransitRewrapResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitRewrapResponse")
.field("ciphertext", &"<redacted>")
.finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct TransitDataKeyRequest {
pub associated_data: Option<SecretString>,
pub context: Option<SecretString>,
pub nonce: Option<SecretString>,
pub bits: Option<u64>,
}
impl TransitDataKeyRequest {
#[cfg(feature = "transit-bytes")]
pub fn with_associated_data_bytes(mut self, associated_data: &[u8]) -> Result<Self> {
self.associated_data = Some(base64_encode_secret(associated_data)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_context_bytes(mut self, context: &[u8]) -> Result<Self> {
self.context = Some(base64_encode_secret(context)?);
Ok(self)
}
#[cfg(feature = "transit-bytes")]
pub fn with_nonce_bytes(mut self, nonce: &[u8]) -> Result<Self> {
self.nonce = Some(base64_encode_secret(nonce)?);
Ok(self)
}
}
#[derive(Clone, Deserialize)]
pub struct TransitDataKeyResponse {
#[serde(default)]
pub plaintext: Option<SecretString>,
pub ciphertext: SecretString,
}
impl fmt::Debug for TransitDataKeyResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitDataKeyResponse")
.field("plaintext", &self.plaintext.as_ref().map(|_| "<redacted>"))
.field("ciphertext", &"<redacted>")
.finish()
}
}
impl TransitDataKeyResponse {
#[cfg(feature = "transit-bytes")]
pub fn plaintext_bytes(&self) -> Result<Option<Zeroizing<Vec<u8>>>> {
self.plaintext
.as_ref()
.map(decode_base64_secret)
.transpose()
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TransitRandomRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<TransitOutputFormat>,
}
#[derive(Clone, Deserialize)]
pub struct TransitRandomResponse {
pub random_bytes: SecretString,
}
impl fmt::Debug for TransitRandomResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitRandomResponse")
.field("random_bytes", &"<redacted>")
.finish()
}
}
impl TransitRandomResponse {
#[cfg(feature = "transit-bytes")]
pub fn random_bytes(&self) -> Result<Zeroizing<Vec<u8>>> {
decode_base64_secret(&self.random_bytes)
}
}
#[derive(Clone, Debug)]
pub struct TransitHashRequest {
pub input: SecretString,
pub format: Option<TransitOutputFormat>,
}
impl TransitHashRequest {
#[cfg(feature = "transit-bytes")]
pub fn from_input_bytes(input: &[u8]) -> Result<Self> {
Ok(Self {
input: base64_encode_secret(input)?,
format: None,
})
}
}
#[derive(Clone, Deserialize)]
pub struct TransitHashResponse {
pub sum: SecretString,
}
impl fmt::Debug for TransitHashResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitHashResponse")
.field("sum", &"<redacted>")
.finish()
}
}
#[derive(Clone, Debug)]
pub struct TransitHmacRequest {
pub input: SecretString,
pub key_version: Option<u64>,
}
impl TransitHmacRequest {
#[cfg(feature = "transit-bytes")]
pub fn from_input_bytes(input: &[u8]) -> Result<Self> {
Ok(Self {
input: base64_encode_secret(input)?,
key_version: None,
})
}
}
#[derive(Clone, Deserialize)]
pub struct TransitHmacResponse {
pub hmac: SecretString,
}
impl fmt::Debug for TransitHmacResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitHmacResponse")
.field("hmac", &"<redacted>")
.finish()
}
}
#[derive(Clone, Debug)]
pub struct TransitSignRequest {
pub input: SecretString,
pub key_version: Option<u64>,
pub context: Option<SecretString>,
pub prehashed: Option<bool>,
pub signature_algorithm: Option<TransitSignatureAlgorithm>,
pub marshaling_algorithm: Option<TransitMarshalingAlgorithm>,
pub salt_length: Option<TransitSaltLength>,
}
impl TransitSignRequest {
pub fn new(input: SecretString) -> Self {
Self {
input,
key_version: None,
context: None,
prehashed: None,
signature_algorithm: None,
marshaling_algorithm: None,
salt_length: None,
}
}
pub fn jws(input: SecretString) -> Self {
Self::new(input).with_marshaling_algorithm(TransitMarshalingAlgorithm::Jws)
}
#[cfg(feature = "transit-bytes")]
pub fn from_input_bytes(input: &[u8]) -> Result<Self> {
Ok(Self::new(base64_encode_secret(input)?))
}
#[cfg(feature = "transit-bytes")]
pub fn with_context_bytes(mut self, context: &[u8]) -> Result<Self> {
self.context = Some(base64_encode_secret(context)?);
Ok(self)
}
#[must_use]
pub fn with_key_version(mut self, key_version: u64) -> Self {
self.key_version = Some(key_version);
self
}
#[must_use]
pub fn with_context(mut self, context: SecretString) -> Self {
self.context = Some(context);
self
}
#[must_use]
pub fn with_prehashed(mut self, prehashed: bool) -> Self {
self.prehashed = Some(prehashed);
self
}
#[must_use]
pub fn with_signature_algorithm(mut self, algorithm: TransitSignatureAlgorithm) -> Self {
self.signature_algorithm = Some(algorithm);
self
}
#[must_use]
pub fn with_marshaling_algorithm(mut self, algorithm: TransitMarshalingAlgorithm) -> Self {
self.marshaling_algorithm = Some(algorithm);
self
}
#[must_use]
pub fn with_salt_length(mut self, salt_length: TransitSaltLength) -> Self {
self.salt_length = Some(salt_length);
self
}
}
#[derive(Clone, Deserialize)]
pub struct TransitSignResponse {
pub signature: SecretString,
#[serde(default, alias = "publickey")]
pub public_key: Option<String>,
}
impl fmt::Debug for TransitSignResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitSignResponse")
.field("signature", &"<redacted>")
.field("public_key", &self.public_key)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct TransitVerifyRequest {
pub input: SecretString,
pub signature: Option<SecretString>,
pub hmac: Option<SecretString>,
pub context: Option<SecretString>,
pub prehashed: Option<bool>,
pub signature_algorithm: Option<TransitSignatureAlgorithm>,
pub marshaling_algorithm: Option<TransitMarshalingAlgorithm>,
pub salt_length: Option<TransitSaltLength>,
}
impl TransitVerifyRequest {
pub fn new(input: SecretString) -> Self {
Self {
input,
signature: None,
hmac: None,
context: None,
prehashed: None,
signature_algorithm: None,
marshaling_algorithm: None,
salt_length: None,
}
}
pub fn from_base64_input_with_signature(input: SecretString, signature: SecretString) -> Self {
Self::new(input).with_signature(signature)
}
pub fn from_base64_input_with_hmac(input: SecretString, hmac: SecretString) -> Self {
Self::new(input).with_hmac(hmac)
}
pub fn jws_with_signature(input: SecretString, signature: SecretString) -> Self {
Self::from_base64_input_with_signature(input, signature)
.with_marshaling_algorithm(TransitMarshalingAlgorithm::Jws)
}
#[cfg(feature = "transit-bytes")]
pub fn from_input_bytes_with_signature(input: &[u8], signature: SecretString) -> Result<Self> {
Ok(Self::from_base64_input_with_signature(
base64_encode_secret(input)?,
signature,
))
}
#[cfg(feature = "transit-bytes")]
pub fn from_input_bytes_with_hmac(input: &[u8], hmac: SecretString) -> Result<Self> {
Ok(Self::from_base64_input_with_hmac(
base64_encode_secret(input)?,
hmac,
))
}
#[cfg(feature = "transit-bytes")]
pub fn with_context_bytes(mut self, context: &[u8]) -> Result<Self> {
self.context = Some(base64_encode_secret(context)?);
Ok(self)
}
#[must_use]
pub fn with_signature(mut self, signature: SecretString) -> Self {
self.signature = Some(signature);
self
}
#[must_use]
pub fn with_hmac(mut self, hmac: SecretString) -> Self {
self.hmac = Some(hmac);
self
}
#[must_use]
pub fn with_context(mut self, context: SecretString) -> Self {
self.context = Some(context);
self
}
#[must_use]
pub fn with_prehashed(mut self, prehashed: bool) -> Self {
self.prehashed = Some(prehashed);
self
}
#[must_use]
pub fn with_signature_algorithm(mut self, algorithm: TransitSignatureAlgorithm) -> Self {
self.signature_algorithm = Some(algorithm);
self
}
#[must_use]
pub fn with_marshaling_algorithm(mut self, algorithm: TransitMarshalingAlgorithm) -> Self {
self.marshaling_algorithm = Some(algorithm);
self
}
#[must_use]
pub fn with_salt_length(mut self, salt_length: TransitSaltLength) -> Self {
self.salt_length = Some(salt_length);
self
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct TransitVerifyResponse {
pub valid: bool,
}
#[derive(Clone, Debug, Default)]
pub struct TransitBatchEncryptRequest {
pub batch_input: Vec<TransitEncryptRequest>,
}
#[derive(Clone, Debug, Default)]
pub struct TransitBatchDecryptRequest {
pub batch_input: Vec<TransitDecryptRequest>,
}
#[derive(Clone, Debug, Default)]
pub struct TransitBatchRewrapRequest {
pub batch_input: Vec<TransitRewrapRequest>,
}
#[derive(Clone, Debug, Default)]
pub struct TransitBatchSignRequest {
pub batch_input: Vec<TransitSignRequest>,
}
#[derive(Clone, Debug, Default)]
pub struct TransitBatchVerifyRequest {
pub batch_input: Vec<TransitVerifyRequest>,
}
macro_rules! impl_transit_batch_request {
($request:ty, $item:ty) => {
impl $request {
pub fn try_push(&mut self, item: $item) -> Result<&mut Self> {
if self.batch_input.len() >= MAX_TRANSIT_BATCH_ITEMS {
return Err(Error::InvalidParameter(
"Transit batch_input exceeds item limit".into(),
));
}
self.batch_input.push(item);
Ok(self)
}
}
};
}
impl_transit_batch_request!(TransitBatchEncryptRequest, TransitEncryptRequest);
impl_transit_batch_request!(TransitBatchDecryptRequest, TransitDecryptRequest);
impl_transit_batch_request!(TransitBatchRewrapRequest, TransitRewrapRequest);
impl_transit_batch_request!(TransitBatchSignRequest, TransitSignRequest);
impl_transit_batch_request!(TransitBatchVerifyRequest, TransitVerifyRequest);
#[derive(Clone, Deserialize)]
pub struct TransitBatchEncryptItem {
#[serde(default)]
pub ciphertext: Option<SecretString>,
#[serde(default)]
pub key_version: Option<u64>,
#[serde(default)]
pub error: Option<String>,
}
impl fmt::Debug for TransitBatchEncryptItem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitBatchEncryptItem")
.field(
"ciphertext",
&self.ciphertext.as_ref().map(|_| "<redacted>"),
)
.field("key_version", &self.key_version)
.field("error", &self.error)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct TransitBatchDecryptItem {
#[serde(default)]
pub plaintext: Option<SecretString>,
#[serde(default)]
pub error: Option<String>,
}
impl fmt::Debug for TransitBatchDecryptItem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitBatchDecryptItem")
.field("plaintext", &self.plaintext.as_ref().map(|_| "<redacted>"))
.field("error", &self.error)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct TransitBatchRewrapItem {
#[serde(default)]
pub ciphertext: Option<SecretString>,
#[serde(default)]
pub error: Option<String>,
}
impl fmt::Debug for TransitBatchRewrapItem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitBatchRewrapItem")
.field(
"ciphertext",
&self.ciphertext.as_ref().map(|_| "<redacted>"),
)
.field("error", &self.error)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct TransitBatchSignItem {
#[serde(default)]
pub signature: Option<SecretString>,
#[serde(default)]
pub error: Option<String>,
}
impl fmt::Debug for TransitBatchSignItem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransitBatchSignItem")
.field("signature", &self.signature.as_ref().map(|_| "<redacted>"))
.field("error", &self.error)
.finish()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchVerifyItem {
#[serde(default)]
pub valid: bool,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchEncryptResponse {
#[serde(default, deserialize_with = "deserialize_bounded_batch_encrypt_vec")]
pub batch_results: Vec<TransitBatchEncryptItem>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchDecryptResponse {
#[serde(default, deserialize_with = "deserialize_bounded_batch_decrypt_vec")]
pub batch_results: Vec<TransitBatchDecryptItem>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchRewrapResponse {
#[serde(default, deserialize_with = "deserialize_bounded_batch_rewrap_vec")]
pub batch_results: Vec<TransitBatchRewrapItem>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchSignResponse {
#[serde(default, deserialize_with = "deserialize_bounded_batch_sign_vec")]
pub batch_results: Vec<TransitBatchSignItem>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TransitBatchVerifyResponse {
#[serde(default, deserialize_with = "deserialize_bounded_batch_verify_vec")]
pub batch_results: Vec<TransitBatchVerifyItem>,
}
#[derive(Serialize)]
struct TransitEncryptPayload<'a> {
plaintext: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
associated_data: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
key_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<&'a str>,
}
#[derive(Serialize)]
struct TransitDecryptPayload<'a> {
ciphertext: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
associated_data: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<&'a str>,
}
#[derive(Serialize)]
struct TransitRewrapPayload<'a> {
ciphertext: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
key_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<&'a str>,
}
#[derive(Serialize)]
struct TransitDataKeyPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
associated_data: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
bits: Option<u64>,
}
#[derive(Serialize)]
struct TransitHashPayload<'a> {
input: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<TransitOutputFormat>,
}
#[derive(Serialize)]
struct TransitHmacPayload<'a> {
input: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
key_version: Option<u64>,
}
#[derive(Serialize)]
struct TransitSignPayload<'a> {
input: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
key_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
prehashed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
signature_algorithm: Option<TransitSignatureAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
marshaling_algorithm: Option<TransitMarshalingAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
salt_length: Option<TransitSaltLength>,
}
#[derive(Serialize)]
struct TransitVerifyPayload<'a> {
input: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
hmac: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
prehashed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
signature_algorithm: Option<TransitSignatureAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
marshaling_algorithm: Option<TransitMarshalingAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
salt_length: Option<TransitSaltLength>,
}
#[derive(Serialize)]
struct TransitBatchPayload<T> {
batch_input: Vec<T>,
}
#[derive(Serialize)]
struct TransitRestorePayload<'a> {
backup: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
force: Option<bool>,
}
#[derive(Serialize)]
struct TransitImportPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
ciphertext: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
public_key: Option<&'a str>,
#[serde(rename = "type")]
key_type: TransitKeyType,
#[serde(skip_serializing_if = "Option::is_none")]
hash_function: Option<TransitImportHashFunction>,
#[serde(skip_serializing_if = "Option::is_none")]
allow_rotation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
derived: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
exportable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
allow_plaintext_backup: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_rotate_period: Option<&'a str>,
}
#[derive(Serialize)]
struct TransitImportVersionPayload<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
ciphertext: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
public_key: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
hash_function: Option<TransitImportHashFunction>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<u64>,
}
impl Client<Authenticated> {
pub fn transit(&self, mount: impl Into<String>) -> Result<Transit<'_>> {
let mount = mount.into();
Ok(Transit {
client: self,
mount: validate_mount_path(&mount)?,
})
}
}
impl Transit<'_> {
pub async fn create_key(&self, name: &str, request: &TransitCreateKeyRequest) -> Result<Empty> {
request.validate()?;
self.client
.request_json(Method::POST, &self.key_path(name, None)?, Some(request))
.await
}
pub async fn read_key(&self, name: &str) -> Result<TransitKeyInfo> {
let envelope: ResponseEnvelope<TransitKeyInfo> = self
.client
.request_json(
Method::GET,
&self.key_path(name, None)?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn wrapping_key(&self) -> Result<TransitWrappingKey> {
let envelope: ResponseEnvelope<TransitWrappingKey> = self
.client
.request_json(
Method::GET,
&self.path(&["wrapping_key"])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn list_keys(&self) -> Result<TransitKeyList> {
self.list_keys_after(None, None).await
}
pub async fn list_keys_after(
&self,
after: Option<&str>,
limit: Option<u64>,
) -> Result<TransitKeyList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
let query = ListPageOptions::from_after_limit(after, limit)?.query_pairs();
let envelope: ResponseEnvelope<TransitKeyList> = self
.client
.request_json_query_accepting(
method,
&self.path(&["keys"])?,
&query,
Option::<&Empty>::None,
&[StatusCode::OK],
)
.await?;
Ok(envelope.data)
}
pub async fn delete_key(&self, name: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.key_path(name, None)?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn update_key(&self, name: &str, request: &TransitUpdateKeyRequest) -> Result<Empty> {
request.validate()?;
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("config"))?,
Some(request),
)
.await
}
pub async fn rotate_key(&self, name: &str) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("rotate"))?,
Option::<&Empty>::None,
)
.await
}
pub async fn import_key(&self, name: &str, request: &TransitImportRequest) -> Result<Empty> {
request.validate()?;
let payload = TransitImportPayload {
ciphertext: request
.ciphertext
.as_ref()
.map(|secret| secret.expose_secret()),
public_key: request.public_key.as_deref(),
key_type: request.key_type,
hash_function: request.hash_function,
allow_rotation: request.allow_rotation,
derived: request.derived,
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
exportable: request.exportable,
allow_plaintext_backup: request.allow_plaintext_backup,
auto_rotate_period: request.auto_rotate_period.as_deref(),
};
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("import"))?,
Some(&payload),
)
.await
}
pub async fn import_key_version(
&self,
name: &str,
request: &TransitImportVersionRequest,
) -> Result<Empty> {
request.validate()?;
let payload = TransitImportVersionPayload {
ciphertext: request
.ciphertext
.as_ref()
.map(|secret| secret.expose_secret()),
public_key: request.public_key.as_deref(),
hash_function: request.hash_function,
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
version: request.version,
};
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("import_version"))?,
Some(&payload),
)
.await
}
pub async fn soft_delete_key(&self, name: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.key_path(name, Some("soft-delete"))?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn restore_soft_deleted_key(&self, name: &str) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("soft-delete-restore"))?,
Option::<&Empty>::None,
)
.await
}
pub async fn byok_export(
&self,
destination: &str,
source: &str,
version: Option<u64>,
hash_function: Option<TransitImportHashFunction>,
) -> Result<TransitByokExport> {
if matches!(version, Some(0)) {
return Err(Error::InvalidParameter(
"Transit key version must be greater than zero".into(),
));
}
let mut segments = vec!["byok-export"];
let destination = validate_key_name(destination)?;
for segment in &destination {
segments.push(segment);
}
let source = validate_key_name(source)?;
for segment in &source {
segments.push(segment);
}
let version = version.map(|value| value.to_string());
if let Some(version) = version.as_deref() {
segments.push(version);
}
let query = hash_function
.map(|hash| vec![("hash", hash.as_query_value().to_owned())])
.unwrap_or_default();
let envelope: ResponseEnvelope<TransitByokExport> = self
.client
.request_json_query_accepting(
Method::GET,
&self.path(&segments)?,
&query,
Option::<&Empty>::None,
&[StatusCode::OK],
)
.await?;
Ok(envelope.data)
}
pub async fn write_global_key_config(
&self,
request: &TransitGlobalKeyConfig,
) -> Result<TransitGlobalKeyConfig> {
let envelope: ResponseEnvelope<TransitGlobalKeyConfig> = self
.client
.request_json(
Method::POST,
&self.path(&["config", "keys"])?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn read_global_key_config(&self) -> Result<TransitGlobalKeyConfig> {
let envelope: ResponseEnvelope<TransitGlobalKeyConfig> = self
.client
.request_json(
Method::GET,
&self.path(&["config", "keys"])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn export_key(
&self,
key_type: TransitExportKeyType,
name: &str,
version: Option<u64>,
) -> Result<TransitExportResponse> {
if matches!(version, Some(0)) {
return Err(Error::InvalidParameter(
"Transit key version must be greater than zero".into(),
));
}
let version = version.map(|version| version.to_string());
let mut segments = vec!["export", key_type.as_path_segment()];
let name = validate_key_name(name)?;
for segment in &name {
segments.push(segment);
}
if let Some(version) = version.as_deref() {
segments.push(version);
}
let envelope: ResponseEnvelope<TransitExportResponse> = self
.client
.request_json(Method::GET, &self.path(&segments)?, Option::<&Empty>::None)
.await?;
Ok(envelope.data)
}
pub async fn backup_key(&self, name: &str) -> Result<TransitBackup> {
let name = validate_key_name(name)?.join("/");
let envelope: ResponseEnvelope<TransitBackup> = self
.client
.request_json(
Method::GET,
&self.path(&["backup", &name])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn restore_key(
&self,
name: Option<&str>,
request: &TransitRestoreRequest,
) -> Result<Empty> {
let payload = TransitRestorePayload {
backup: request.backup.expose_secret(),
force: request.force,
};
let path = match name {
Some(name) => {
let name = validate_key_name(name)?.join("/");
self.path(&["restore", &name])?
}
None => self.path(&["restore"])?,
};
self.client
.request_json(Method::POST, &path, Some(&payload))
.await
}
pub async fn trim_key(&self, name: &str, request: &TransitTrimRequest) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.key_path(name, Some("trim"))?,
Some(request),
)
.await
}
pub async fn write_cache_config(
&self,
request: &TransitCacheConfig,
) -> Result<TransitCacheConfig> {
let request = TransitCacheConfig::new(request.size)?;
let envelope: ResponseEnvelope<TransitCacheConfig> = self
.client
.request_json(Method::POST, &self.path(&["cache-config"])?, Some(&request))
.await?;
Ok(envelope.data)
}
pub async fn read_cache_config(&self) -> Result<TransitCacheConfig> {
let envelope: ResponseEnvelope<TransitCacheConfig> = self
.client
.request_json(
Method::GET,
&self.path(&["cache-config"])?,
Option::<&Empty>::None,
)
.await?;
Ok(envelope.data)
}
pub async fn generate_csr(
&self,
name: &str,
request: &TransitCsrRequest,
) -> Result<TransitCsrResponse> {
let envelope: ResponseEnvelope<TransitCsrResponse> = self
.client
.request_json(
Method::POST,
&self.key_path(name, Some("csr"))?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn set_certificate(
&self,
name: &str,
request: &TransitSetCertificateRequest,
) -> Result<TransitCertificateChain> {
let envelope: ResponseEnvelope<TransitCertificateChain> = self
.client
.request_json(
Method::POST,
&self.key_path(name, Some("set-certificate"))?,
Some(request),
)
.await?;
Ok(envelope.data)
}
pub async fn encrypt(
&self,
name: &str,
request: &TransitEncryptRequest,
) -> Result<TransitEncryptResponse> {
let payload = encrypt_payload(request);
self.enveloped(
Method::POST,
&self.operation_path("encrypt", name, None)?,
&payload,
)
.await
}
pub async fn decrypt(
&self,
name: &str,
request: &TransitDecryptRequest,
) -> Result<TransitDecryptResponse> {
let payload = decrypt_payload(request);
self.enveloped(
Method::POST,
&self.operation_path("decrypt", name, None)?,
&payload,
)
.await
}
pub async fn rewrap(
&self,
name: &str,
request: &TransitRewrapRequest,
) -> Result<TransitRewrapResponse> {
let payload = rewrap_payload(request);
self.enveloped(
Method::POST,
&self.operation_path("rewrap", name, None)?,
&payload,
)
.await
}
pub async fn data_key(
&self,
name: &str,
data_key_type: TransitDataKeyType,
request: &TransitDataKeyRequest,
) -> Result<TransitDataKeyResponse> {
let payload = TransitDataKeyPayload {
associated_data: request
.associated_data
.as_ref()
.map(|secret| secret.expose_secret()),
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
nonce: request.nonce.as_ref().map(|secret| secret.expose_secret()),
bits: request.bits,
};
self.enveloped(
Method::POST,
&self.path(&["datakey", data_key_type.as_path_segment(), name])?,
&payload,
)
.await
}
pub async fn random(
&self,
bytes: Option<u64>,
request: &TransitRandomRequest,
) -> Result<TransitRandomResponse> {
let bytes = bytes.map(|value| value.to_string());
let mut segments = vec!["random"];
if let Some(bytes) = bytes.as_deref() {
segments.push(bytes);
}
self.enveloped(Method::POST, &self.path(&segments)?, request)
.await
}
pub async fn random_from_source(
&self,
source: TransitRandomSource,
bytes: Option<u64>,
request: &TransitRandomRequest,
) -> Result<TransitRandomResponse> {
let bytes = bytes.map(|value| value.to_string());
let mut segments = vec!["random", source.as_path_segment()];
if let Some(bytes) = bytes.as_deref() {
segments.push(bytes);
}
self.enveloped(Method::POST, &self.path(&segments)?, request)
.await
}
pub async fn hash(
&self,
algorithm: TransitHashAlgorithm,
request: &TransitHashRequest,
) -> Result<TransitHashResponse> {
let payload = TransitHashPayload {
input: request.input.expose_secret(),
format: request.format,
};
self.enveloped(
Method::POST,
&self.path(&["hash", algorithm.as_path_segment()])?,
&payload,
)
.await
}
pub async fn hmac(
&self,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
request: &TransitHmacRequest,
) -> Result<TransitHmacResponse> {
let payload = TransitHmacPayload {
input: request.input.expose_secret(),
key_version: request.key_version,
};
self.enveloped(
Method::POST,
&self.operation_path("hmac", name, algorithm)?,
&payload,
)
.await
}
pub async fn sign(
&self,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
request: &TransitSignRequest,
) -> Result<TransitSignResponse> {
let payload = sign_payload(request);
self.enveloped(
Method::POST,
&self.operation_path("sign", name, algorithm)?,
&payload,
)
.await
}
pub async fn verify(
&self,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
request: &TransitVerifyRequest,
) -> Result<TransitVerifyResponse> {
let payload = verify_payload(request);
self.enveloped(
Method::POST,
&self.operation_path("verify", name, algorithm)?,
&payload,
)
.await
}
pub async fn batch_encrypt(
&self,
name: &str,
request: &TransitBatchEncryptRequest,
) -> Result<TransitBatchEncryptResponse> {
validate_batch_len(request.batch_input.len())?;
let payload = TransitBatchPayload {
batch_input: request
.batch_input
.iter()
.map(encrypt_payload)
.collect::<Vec<_>>(),
};
self.enveloped(
Method::POST,
&self.operation_path("encrypt", name, None)?,
&payload,
)
.await
}
pub async fn batch_decrypt(
&self,
name: &str,
request: &TransitBatchDecryptRequest,
) -> Result<TransitBatchDecryptResponse> {
validate_batch_len(request.batch_input.len())?;
let payload = TransitBatchPayload {
batch_input: request
.batch_input
.iter()
.map(decrypt_payload)
.collect::<Vec<_>>(),
};
self.enveloped(
Method::POST,
&self.operation_path("decrypt", name, None)?,
&payload,
)
.await
}
pub async fn batch_rewrap(
&self,
name: &str,
request: &TransitBatchRewrapRequest,
) -> Result<TransitBatchRewrapResponse> {
validate_batch_len(request.batch_input.len())?;
let payload = TransitBatchPayload {
batch_input: request
.batch_input
.iter()
.map(rewrap_payload)
.collect::<Vec<_>>(),
};
self.enveloped(
Method::POST,
&self.operation_path("rewrap", name, None)?,
&payload,
)
.await
}
pub async fn batch_sign(
&self,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
request: &TransitBatchSignRequest,
) -> Result<TransitBatchSignResponse> {
validate_batch_len(request.batch_input.len())?;
let payload = TransitBatchPayload {
batch_input: request
.batch_input
.iter()
.map(sign_payload)
.collect::<Vec<_>>(),
};
self.enveloped(
Method::POST,
&self.operation_path("sign", name, algorithm)?,
&payload,
)
.await
}
pub async fn batch_verify(
&self,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
request: &TransitBatchVerifyRequest,
) -> Result<TransitBatchVerifyResponse> {
validate_batch_len(request.batch_input.len())?;
let payload = TransitBatchPayload {
batch_input: request
.batch_input
.iter()
.map(verify_payload)
.collect::<Vec<_>>(),
};
self.enveloped(
Method::POST,
&self.operation_path("verify", name, algorithm)?,
&payload,
)
.await
}
async fn enveloped<T, B>(&self, method: Method, path: &str, request: &B) -> Result<T>
where
T: for<'de> Deserialize<'de>,
B: Serialize + ?Sized,
{
let envelope: ResponseEnvelope<T> = self
.client
.request_json(method, path, Some(request))
.await?;
Ok(envelope.data)
}
fn key_path(&self, name: &str, suffix: Option<&str>) -> Result<String> {
let mut segments = vec!["keys"];
let name = validate_key_name(name)?;
for segment in &name {
segments.push(segment);
}
if let Some(suffix) = suffix {
segments.push(suffix);
}
self.path(&segments)
}
fn operation_path(
&self,
operation: &'static str,
name: &str,
algorithm: Option<TransitHashAlgorithm>,
) -> Result<String> {
let mut segments = vec![operation];
let name = validate_key_name(name)?;
for segment in &name {
segments.push(segment);
}
if let Some(algorithm) = algorithm {
segments.push(algorithm.as_path_segment());
}
self.path(&segments)
}
fn path(&self, tail: &[&str]) -> Result<String> {
let mut segments = self.mount.clone();
for segment in tail {
segments.extend(validate_endpoint_path(segment)?);
}
Ok(segments.join("/"))
}
}
fn validate_key_name(name: &str) -> Result<Vec<String>> {
validate_mount_path(name)
}
fn validate_batch_len(len: usize) -> Result<()> {
if len == 0 {
return Err(Error::InvalidParameter(
"Transit batch_input must not be empty".into(),
));
}
if len > MAX_TRANSIT_BATCH_ITEMS {
return Err(Error::InvalidParameter(
"Transit batch_input exceeds item limit".into(),
));
}
Ok(())
}
fn validate_non_empty_secret(secret: &SecretString, label: &'static str) -> Result<()> {
if secret.expose_secret().is_empty() {
return Err(Error::InvalidParameter(format!(
"{label} must not be empty"
)));
}
Ok(())
}
fn validate_non_empty_public_key(public_key: &str, label: &'static str) -> Result<()> {
if public_key.trim().is_empty() {
return Err(Error::InvalidParameter(format!(
"{label} must not be empty"
)));
}
Ok(())
}
fn validate_import_material(
ciphertext: Option<&SecretString>,
public_key: Option<&str>,
label: &'static str,
) -> Result<()> {
match (ciphertext, public_key) {
(Some(ciphertext), None) => {
validate_non_empty_secret(ciphertext, "Transit import ciphertext")
}
(None, Some(public_key)) => {
validate_non_empty_public_key(public_key, "Transit import public_key")
}
(Some(_), Some(_)) => Err(Error::InvalidParameter(format!(
"{label} must use either ciphertext or public_key, not both"
))),
(None, None) => Err(Error::InvalidParameter(format!(
"{label} requires ciphertext or public_key"
))),
}
}
fn encrypt_payload(request: &TransitEncryptRequest) -> TransitEncryptPayload<'_> {
TransitEncryptPayload {
plaintext: request.plaintext.expose_secret(),
associated_data: request
.associated_data
.as_ref()
.map(|secret| secret.expose_secret()),
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
key_version: request.key_version,
nonce: request.nonce.as_ref().map(|secret| secret.expose_secret()),
}
}
fn decrypt_payload(request: &TransitDecryptRequest) -> TransitDecryptPayload<'_> {
TransitDecryptPayload {
ciphertext: request.ciphertext.expose_secret(),
associated_data: request
.associated_data
.as_ref()
.map(|secret| secret.expose_secret()),
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
nonce: request.nonce.as_ref().map(|secret| secret.expose_secret()),
}
}
fn rewrap_payload(request: &TransitRewrapRequest) -> TransitRewrapPayload<'_> {
TransitRewrapPayload {
ciphertext: request.ciphertext.expose_secret(),
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
key_version: request.key_version,
nonce: request.nonce.as_ref().map(|secret| secret.expose_secret()),
}
}
fn sign_payload(request: &TransitSignRequest) -> TransitSignPayload<'_> {
TransitSignPayload {
input: request.input.expose_secret(),
key_version: request.key_version,
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
prehashed: request.prehashed,
signature_algorithm: request.signature_algorithm,
marshaling_algorithm: request.marshaling_algorithm,
salt_length: request.salt_length,
}
}
fn verify_payload(request: &TransitVerifyRequest) -> TransitVerifyPayload<'_> {
TransitVerifyPayload {
input: request.input.expose_secret(),
signature: request
.signature
.as_ref()
.map(|secret| secret.expose_secret()),
hmac: request.hmac.as_ref().map(|secret| secret.expose_secret()),
context: request
.context
.as_ref()
.map(|secret| secret.expose_secret()),
prehashed: request.prehashed,
signature_algorithm: request.signature_algorithm,
marshaling_algorithm: request.marshaling_algorithm,
salt_length: request.salt_length,
}
}
#[cfg(feature = "transit-import")]
fn wrap_import_key_material<R>(
rng: &mut R,
wrapping_key_pem: &str,
mut key_material: Zeroizing<Vec<u8>>,
hash_function: TransitImportHashFunction,
) -> Result<TransitWrappedImportKey>
where
R: rand::CryptoRng + ?Sized,
{
if key_material.is_empty() {
return Err(Error::InvalidParameter(
"Transit import key material must not be empty".into(),
));
}
if key_material.len() > MAX_TRANSIT_IMPORT_KEY_MATERIAL_BYTES {
return Err(Error::InvalidParameter(
"Transit import key material exceeds local helper limit".into(),
));
}
use aes_kw::{KeyInit, KwpAes256};
let mut ephemeral_aes_key = Zeroizing::new([0_u8; TRANSIT_IMPORT_EPHEMERAL_AES_KEY_BYTES]);
rng.fill_bytes(&mut ephemeral_aes_key[..]);
let kwp = KwpAes256::new((&*ephemeral_aes_key).into());
let wrapped_target_len = (key_material.len().div_ceil(8) + 1) * 8;
let mut wrapped_target = Zeroizing::new(vec![0_u8; wrapped_target_len]);
let wrapped_target_len = kwp
.wrap_key(&key_material, &mut wrapped_target)
.map_err(|_| {
Error::InvalidParameter("Transit import key material could not be wrapped".into())
})?
.len();
wrapped_target.truncate(wrapped_target_len);
key_material.zeroize();
let wrapped_aes_key =
rsa_oaep_wrap_aes_key(wrapping_key_pem, &ephemeral_aes_key[..], hash_function)?;
let mut combined = Zeroizing::new(Vec::with_capacity(
wrapped_aes_key.len() + wrapped_target.len(),
));
combined.extend_from_slice(&wrapped_aes_key);
combined.extend_from_slice(&wrapped_target);
let ciphertext = base64_encode_secret(&combined)?;
Ok(TransitWrappedImportKey {
ciphertext,
hash_function,
})
}
#[cfg(feature = "transit-import")]
fn rsa_oaep_wrap_aes_key(
wrapping_key_pem: &str,
ephemeral_aes_key: &[u8],
hash_function: TransitImportHashFunction,
) -> Result<Zeroizing<Vec<u8>>> {
use openssl::{
md::Md,
pkey::{Id, PKey, Public},
pkey_ctx::PkeyCtx,
rsa::Padding,
};
let wrapping_key: PKey<Public> = PKey::public_key_from_pem(wrapping_key_pem.as_bytes())
.map_err(|_| Error::InvalidParameter("Transit wrapping key PEM is invalid".into()))?;
if wrapping_key.id() != Id::RSA {
return Err(Error::InvalidParameter(
"Transit wrapping key must be an RSA public key".into(),
));
}
if wrapping_key.size() != TRANSIT_WRAPPING_KEY_BYTES {
return Err(Error::InvalidParameter(
"Transit wrapping key must be a 4096-bit RSA public key".into(),
));
}
let digest = match hash_function {
#[cfg(feature = "allow-sha1-acknowledged")]
#[allow(deprecated)]
TransitImportHashFunction::Sha1 => Md::sha1(),
TransitImportHashFunction::Sha224 => Md::sha224(),
TransitImportHashFunction::Sha256 => Md::sha256(),
TransitImportHashFunction::Sha384 => Md::sha384(),
TransitImportHashFunction::Sha512 => Md::sha512(),
};
let mut context = PkeyCtx::new(&wrapping_key)
.map_err(|_| Error::InvalidParameter("Transit wrapping key is invalid".into()))?;
context
.encrypt_init()
.map_err(|_| Error::InvalidParameter("Transit wrapping key encryption failed".into()))?;
context
.set_rsa_padding(Padding::PKCS1_OAEP)
.map_err(|_| Error::InvalidParameter("Transit wrapping key encryption failed".into()))?;
context
.set_rsa_oaep_md(digest)
.map_err(|_| Error::InvalidParameter("Transit wrapping key encryption failed".into()))?;
context
.set_rsa_mgf1_md(digest)
.map_err(|_| Error::InvalidParameter("Transit wrapping key encryption failed".into()))?;
let mut encrypted = Zeroizing::new(Vec::new());
context
.encrypt_to_vec(ephemeral_aes_key, &mut encrypted)
.map_err(|_| Error::InvalidParameter("Transit wrapping key encryption failed".into()))?;
Ok(encrypted)
}
#[cfg(any(feature = "transit-bytes", feature = "transit-import"))]
fn base64_encode_secret(input: &[u8]) -> Result<SecretString> {
let encoded = base64_ng::STANDARD
.encode_secret(input)
.map_err(|_| Error::InvalidParameter("base64 input is too large".into()))?;
let exposed = encoded.try_into_exposed_string().map_err(|_| {
Error::Internal("base64-ng produced non-UTF-8 text for standard base64 output")
})?;
Ok(SecretString::from(
exposed.into_exposed_unprotected_string_caller_must_zeroize(),
))
}
#[cfg(feature = "transit-bytes")]
fn decode_base64_secret(input: &SecretString) -> Result<Zeroizing<Vec<u8>>> {
let decoded = base64_ng::STANDARD
.decode_secret(input.expose_secret().as_bytes())
.map_err(|_| Error::Decode("OpenBao response contained invalid base64".into()))?;
let exposed = decoded.into_exposed_vec();
Ok(Zeroizing::new(
exposed.into_exposed_unprotected_vec_caller_must_zeroize(),
))
}
fn deserialize_bounded_u64_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, u64>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(BoundedU64MapVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
fn deserialize_bounded_secret_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, SecretString>, D::Error>
where
D: Deserializer<'de>,
{
deserializer
.deserialize_map(BoundedSecretMapVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
fn deserialize_bounded_batch_encrypt_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<TransitBatchEncryptItem>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_bounded_vec(deserializer)
}
fn deserialize_bounded_batch_decrypt_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<TransitBatchDecryptItem>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_bounded_vec(deserializer)
}
fn deserialize_bounded_batch_rewrap_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<TransitBatchRewrapItem>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_bounded_vec(deserializer)
}
fn deserialize_bounded_batch_sign_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<TransitBatchSignItem>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_bounded_vec(deserializer)
}
fn deserialize_bounded_batch_verify_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<TransitBatchVerifyItem>, D::Error>
where
D: Deserializer<'de>,
{
deserialize_bounded_vec(deserializer)
}
fn deserialize_bounded_vec<'de, D, T>(deserializer: D) -> core::result::Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
deserializer.deserialize_seq(
BoundedVecVisitor::<T, { crate::response::MAX_RESPONSE_STRINGS }> {
_marker: core::marker::PhantomData,
},
)
}
struct BoundedU64MapVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedU64MapVisitor<MAX> {
type Value = BTreeMap<String, u64>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a map of at most {MAX} integer values")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < MAX {
let Some((key, value)) = map.next_entry::<String, u64>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
return Err(A::Error::custom("OpenBao integer map exceeds item limit"));
}
Ok(values)
}
}
struct BoundedSecretMapVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedSecretMapVisitor<MAX> {
type Value = BTreeMap<String, SecretString>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a map of at most {MAX} secret string values")
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < MAX {
let Some((key, value)) = map.next_entry::<String, SecretString>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
return Err(A::Error::custom("OpenBao secret map exceeds item limit"));
}
Ok(values)
}
}
struct BoundedVecVisitor<T, const MAX: usize> {
_marker: core::marker::PhantomData<T>,
}
impl<'de, T, const MAX: usize> Visitor<'de> for BoundedVecVisitor<T, MAX>
where
T: Deserialize<'de>,
{
type Value = Vec<T>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a list of at most {MAX} values")
}
fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::new();
while values.len() < MAX {
let Some(value) = seq.next_element::<T>()? else {
return Ok(values);
};
values.push(value);
}
if seq.next_element::<IgnoredAny>()?.is_some() {
return Err(A::Error::custom("OpenBao batch result exceeds item limit"));
}
Ok(values)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(deprecated)]
use std::collections::BTreeMap;
#[cfg(feature = "transit-bytes")]
use secrecy::ExposeSecret;
use secrecy::SecretString;
use crate::{Client, OpenBaoConfig};
#[cfg(feature = "transit-bytes")]
use super::{TransitDecryptResponse, TransitEncryptRequest, TransitHashRequest};
use super::{TransitEncryptResponse, TransitKeyInfo, TransitKeyList};
#[test]
fn transit_paths_are_validated() {
let config = OpenBaoConfig::new("http://127.0.0.1:8200")
.and_then(OpenBaoConfig::allow_localhost_http)
.unwrap_or_else(|error| panic!("{error}"));
let client = Client::from_config(config)
.unwrap_or_else(|error| panic!("{error}"))
.with_token(SecretString::from("token"));
let transit = client
.transit("transit")
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
transit
.operation_path("encrypt", "app-key", None)
.unwrap_or_else(|error| panic!("{error}")),
"transit/encrypt/app-key"
);
assert!(
transit
.operation_path("encrypt", "../app-key", None)
.is_err()
);
assert!(client.transit("../transit").is_err());
}
#[test]
fn transit_key_list_is_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("key-{index}"));
}
let value = serde_json::json!({ "keys": keys });
let error = match serde_json::from_value::<TransitKeyList>(value) {
Ok(_) => panic!("oversized Transit key list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn transit_create_key_validates_direct_auto_rotate_period_assignment() {
let request = super::TransitCreateKeyRequest {
auto_rotate_period: Some("forever".to_owned()),
..super::TransitCreateKeyRequest::default()
};
assert!(request.validate().is_err());
}
#[test]
fn transit_key_version_map_is_bounded() {
let mut keys = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.insert(index.to_string(), serde_json::json!(1));
}
let value = serde_json::json!({
"type": "aes256-gcm96",
"keys": keys,
});
let error = match serde_json::from_value::<TransitKeyInfo>(value) {
Ok(_) => panic!("oversized Transit key version map unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn transit_encrypt_debug_redacts_ciphertext() {
let response = TransitEncryptResponse {
ciphertext: SecretString::from("vault:v1:secret-ciphertext"),
key_version: Some(1),
};
let debug = format!("{response:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("secret-ciphertext"));
}
#[test]
fn transit_import_requests_reject_empty_ciphertext_and_redact_debug() {
assert!(
super::TransitImportRequest::new(
SecretString::from(""),
super::TransitKeyType::Aes256Gcm96
)
.is_err()
);
assert!(super::TransitImportVersionRequest::new(SecretString::from("")).is_err());
let request = super::TransitImportRequest::new(
SecretString::from("wrapped-secret"),
super::TransitKeyType::Aes256Gcm96,
)
.unwrap_or_else(|error| panic!("{error}"))
.with_context(SecretString::from("secret-context"));
let debug = format!("{request:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("wrapped-secret"));
assert!(!debug.contains("secret-context"));
let public_request = super::TransitImportRequest::from_public_key(
"-----BEGIN PUBLIC KEY-----",
super::TransitKeyType::Rsa2048,
)
.unwrap_or_else(|error| panic!("{error}"));
assert!(public_request.ciphertext.is_none());
assert!(public_request.public_key.is_some());
assert!(
super::TransitImportRequest::from_public_key("", super::TransitKeyType::Rsa2048)
.is_err()
);
let public_version =
super::TransitImportVersionRequest::from_public_key("-----BEGIN PUBLIC KEY-----")
.unwrap_or_else(|error| panic!("{error}"));
assert!(public_version.ciphertext.is_none());
assert!(public_version.public_key.is_some());
assert!(super::TransitImportVersionRequest::from_public_key("").is_err());
let mut conflicting_request = super::TransitImportRequest::new(
SecretString::from("wrapped-secret"),
super::TransitKeyType::Aes256Gcm96,
)
.unwrap_or_else(|error| panic!("{error}"));
conflicting_request.public_key = Some("-----BEGIN PUBLIC KEY-----".to_owned());
assert!(conflicting_request.validate().is_err());
let mut missing_material =
super::TransitImportVersionRequest::new(SecretString::from("wrapped-version"))
.unwrap_or_else(|error| panic!("{error}"));
missing_material.ciphertext = None;
assert!(missing_material.validate().is_err());
}
#[test]
fn transit_batch_try_push_enforces_item_limit() {
let mut request = super::TransitBatchEncryptRequest::default();
for _ in 0..super::MAX_TRANSIT_BATCH_ITEMS {
request
.try_push(super::TransitEncryptRequest::new(SecretString::from(
"cGxhaW4=",
)))
.unwrap_or_else(|error| panic!("{error}"));
}
assert!(
request
.try_push(super::TransitEncryptRequest::new(SecretString::from(
"cGxhaW4="
)))
.is_err()
);
assert_eq!(request.batch_input.len(), super::MAX_TRANSIT_BATCH_ITEMS);
}
#[cfg(feature = "transit-import")]
#[test]
fn transit_import_helper_wraps_key_material_without_leaking_debug() {
use openssl::rsa::Rsa;
use secrecy::ExposeSecret;
use zeroize::Zeroizing;
let mut rng = rand::rng();
let public_key_pem = Rsa::generate(4096)
.unwrap_or_else(|error| panic!("{error}"))
.public_key_to_pem()
.unwrap_or_else(|error| panic!("{error}"));
let public_key_pem =
String::from_utf8(public_key_pem).unwrap_or_else(|error| panic!("{error}"));
let wrapped = super::TransitWrappedImportKey::wrap_key_material_with_rng(
&mut rng,
&public_key_pem,
Zeroizing::new(b"key-material-for-import".to_vec()),
super::TransitImportHashFunction::Sha256,
)
.unwrap_or_else(|error| panic!("{error}"));
assert!(!wrapped.ciphertext.expose_secret().is_empty());
let debug = format!("{wrapped:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains(wrapped.ciphertext.expose_secret()));
let request = wrapped
.clone()
.into_import_request(super::TransitKeyType::Aes256Gcm96)
.unwrap_or_else(|error| panic!("{error}"));
assert!(request.ciphertext.is_some());
assert_eq!(
request.hash_function,
Some(super::TransitImportHashFunction::Sha256)
);
let version_request = wrapped
.into_import_version_request()
.unwrap_or_else(|error| panic!("{error}"));
assert!(version_request.ciphertext.is_some());
assert_eq!(
version_request.hash_function,
Some(super::TransitImportHashFunction::Sha256)
);
}
#[cfg(feature = "transit-import")]
#[test]
fn transit_import_helper_rejects_non_rsa_wrapping_key() {
use openssl::{ec::EcKey, nid::Nid};
use zeroize::Zeroizing;
let ec_group = openssl::ec::EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)
.unwrap_or_else(|error| panic!("{error}"));
let public_key_pem = EcKey::generate(&ec_group)
.unwrap_or_else(|error| panic!("{error}"))
.public_key_to_pem()
.unwrap_or_else(|error| panic!("{error}"));
let public_key_pem =
String::from_utf8(public_key_pem).unwrap_or_else(|error| panic!("{error}"));
let error = match super::TransitWrappedImportKey::wrap_key_material(
&public_key_pem,
Zeroizing::new(b"key-material-for-import".to_vec()),
super::TransitImportHashFunction::Sha256,
) {
Ok(_) => panic!("non-RSA wrapping key was unexpectedly accepted"),
Err(error) => error,
};
assert!(error.to_string().contains("RSA public key"));
}
#[test]
fn transit_byok_export_redacts_wrapped_blobs() {
let response = super::TransitByokExport {
name: Some("key".to_owned()),
keys: BTreeMap::from([("1".to_owned(), SecretString::from("wrapped-secret"))]),
};
let debug = format!("{response:?}");
assert!(debug.contains("<1 redacted>"));
assert!(!debug.contains("wrapped-secret"));
}
#[test]
fn transit_cache_config_validates_small_nonzero_sizes() {
assert!(super::TransitCacheConfig::new(0).is_ok());
assert!(super::TransitCacheConfig::new(10).is_ok());
assert!(super::TransitCacheConfig::new(9).is_err());
}
#[cfg(feature = "transit-bytes")]
#[test]
fn transit_byte_helpers_use_base64_ng_and_zeroizing_decode() {
let request = TransitEncryptRequest::from_plaintext_bytes(b"secret")
.unwrap_or_else(|error| panic!("{error}"))
.with_context_bytes(b"app")
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(request.plaintext.expose_secret(), "c2VjcmV0");
assert_eq!(
request.context.as_ref().map(SecretString::expose_secret),
Some("YXBw")
);
let hash = TransitHashRequest::from_input_bytes(b"payload")
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(hash.input.expose_secret(), "cGF5bG9hZA==");
let response = TransitDecryptResponse {
plaintext: SecretString::from("c2VjcmV0"),
};
let bytes = response
.plaintext_bytes()
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(&bytes[..], b"secret");
}
}