use std::fmt;
use aead::{Aead, KeyInit};
use aes_gcm_siv::{Aes256GcmSiv, Nonce};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use ed25519_dalek::{Signature, Signer, SigningKey};
use sha2::{Digest, Sha256};
use tonic::Status;
use uuid::Uuid;
use crate::runtime::DataBrokerRuntime;
use super::config::{
DEFAULT_TRANSIT_ALGORITHM, SIGNING_TRANSIT_ALGORITHM, SUPPORTED_TRANSIT_ALGORITHMS,
VAULT_ED25519_PREFIX, VAULT_HMAC_PREFIX, VAULT_TRANSIT_ENVELOPE_PREFIX,
};
use super::errors::{
vault_field_violation, vault_internal_status, vault_master_key_operation_status,
};
pub(crate) struct DataKey(pub(crate) [u8; 32]);
impl fmt::Debug for DataKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DataKey")
.field("key", &"[redacted]")
.finish()
}
}
impl DataKey {
pub(crate) fn generate() -> Self {
let mut bytes = [0u8; 32];
bytes[..16].copy_from_slice(Uuid::new_v4().as_bytes());
bytes[16..].copy_from_slice(Uuid::new_v4().as_bytes());
DataKey(bytes)
}
pub(crate) fn to_b64(&self) -> String {
BASE64_STANDARD.encode(self.0)
}
fn from_b64(value: &str) -> Result<Self, Status> {
let raw = BASE64_STANDARD.decode(value.trim()).map_err(|err| {
vault_internal_status(
"data_key_decode",
format!("vault data-key decode failed: {err}"),
)
})?;
let bytes: [u8; 32] = raw.try_into().map_err(|_| {
vault_internal_status("data_key_decode", "vault data-key has an invalid length")
})?;
Ok(DataKey(bytes))
}
}
pub(crate) struct PlaintextSecret(pub(crate) String);
impl fmt::Debug for PlaintextSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("PlaintextSecret")
.field(&"[redacted]")
.finish()
}
}
pub(crate) fn wrap_dek(runtime: &DataBrokerRuntime, dek: &DataKey) -> Result<String, Status> {
runtime
.encrypt_secret_at_rest(&dek.to_b64())
.map_err(|err| {
vault_master_key_operation_status(
"wrap_data_key",
format!("vault is sealed: cannot wrap data key ({err})"),
)
})
}
pub(crate) fn unwrap_dek(runtime: &DataBrokerRuntime, wrapped: &str) -> Result<DataKey, Status> {
let b64 = runtime.decrypt_secret_at_rest(wrapped).map_err(|err| {
vault_master_key_operation_status(
"unwrap_data_key",
format!("vault is sealed: cannot unwrap data key ({err})"),
)
})?;
DataKey::from_b64(&b64)
}
pub(crate) fn dek_seal(dek: &DataKey, version: i64, plaintext: &[u8]) -> Result<String, Status> {
let cipher = Aes256GcmSiv::new_from_slice(&dek.0).map_err(|err| {
vault_internal_status(
"seal_transit_payload",
format!("vault AEAD key invalid: {err}"),
)
})?;
let mut nonce = [0u8; 12];
nonce.copy_from_slice(&Uuid::new_v4().as_bytes()[..12]);
let ciphertext = cipher
.encrypt(Nonce::from_slice(&nonce), plaintext)
.map_err(|err| {
vault_internal_status(
"seal_transit_payload",
format!("vault encrypt failed: {err}"),
)
})?;
let mut envelope = Vec::with_capacity(nonce.len() + ciphertext.len());
envelope.extend_from_slice(&nonce);
envelope.extend_from_slice(&ciphertext);
Ok(format!(
"{VAULT_TRANSIT_ENVELOPE_PREFIX}{version}:{}",
BASE64_STANDARD.encode(envelope)
))
}
pub(crate) fn dek_open(dek: &DataKey, encoded: &str) -> Result<Vec<u8>, Status> {
let envelope = BASE64_STANDARD.decode(encoded).map_err(|err| {
vault_field_violation(
"ciphertext",
"must be base64-encoded vault transit ciphertext bytes",
format!("vault ciphertext decode failed: {err}"),
)
})?;
if envelope.len() <= 12 {
return Err(vault_field_violation(
"ciphertext",
"must include a 12-byte nonce and encrypted payload",
"vault ciphertext envelope is too short",
));
}
let (nonce, ciphertext) = envelope.split_at(12);
let cipher = Aes256GcmSiv::new_from_slice(&dek.0).map_err(|err| {
vault_internal_status(
"open_transit_payload",
format!("vault AEAD key invalid: {err}"),
)
})?;
cipher
.decrypt(Nonce::from_slice(nonce), ciphertext)
.map_err(|_| {
vault_field_violation(
"ciphertext",
"must decrypt with the selected vault transit key version",
"vault decrypt failed (wrong key version or corrupt ciphertext)",
)
})
}
pub(crate) fn parse_transit_envelope(value: &str) -> Option<(i64, &str)> {
let rest = value.strip_prefix(VAULT_TRANSIT_ENVELOPE_PREFIX)?;
let (version, encoded) = rest.split_once(':')?;
Some((version.parse().ok()?, encoded))
}
pub(crate) fn parse_mac_envelope(value: &str) -> Option<(i64, &str)> {
let rest = value.strip_prefix(VAULT_HMAC_PREFIX)?;
let (version, encoded) = rest.split_once(':')?;
Some((version.parse().ok()?, encoded))
}
pub(crate) fn parse_ed25519_envelope(value: &str) -> Option<(i64, &str)> {
let rest = value.strip_prefix(VAULT_ED25519_PREFIX)?;
let (version, encoded) = rest.split_once(':')?;
Some((version.parse().ok()?, encoded))
}
pub(crate) fn ed25519_sign_b64(seed: &[u8; 32], message: &[u8]) -> String {
let signing_key = SigningKey::from_bytes(seed);
BASE64_STANDARD.encode(signing_key.sign(message).to_bytes())
}
pub(crate) fn ed25519_verify_b64(seed: &[u8; 32], message: &[u8], signature_b64: &str) -> bool {
let Ok(bytes) = BASE64_STANDARD.decode(signature_b64) else {
return false;
};
let Ok(signature_bytes): Result<[u8; 64], _> = bytes.try_into() else {
return false;
};
let signature = Signature::from_bytes(&signature_bytes);
SigningKey::from_bytes(seed)
.verifying_key()
.verify_strict(message, &signature)
.is_ok()
}
pub(crate) fn ed25519_public_key_b64(seed: &[u8; 32]) -> String {
BASE64_STANDARD.encode(SigningKey::from_bytes(seed).verifying_key().to_bytes())
}
const HMAC_BLOCK: usize = 64;
pub(crate) fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block_key = [0u8; HMAC_BLOCK];
if key.len() > HMAC_BLOCK {
let digest = Sha256::digest(key);
block_key[..32].copy_from_slice(&digest);
} else {
block_key[..key.len()].copy_from_slice(key);
}
let mut ipad = [0x36u8; HMAC_BLOCK];
let mut opad = [0x5cu8; HMAC_BLOCK];
for i in 0..HMAC_BLOCK {
ipad[i] ^= block_key[i];
opad[i] ^= block_key[i];
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(message);
let inner_digest = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner_digest);
let mut out = [0u8; 32];
out.copy_from_slice(&outer.finalize());
out
}
pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub(crate) fn validate_transit_algorithm(requested: &str) -> Result<String, Status> {
let trimmed = requested.trim();
if trimmed.is_empty() {
return Ok(DEFAULT_TRANSIT_ALGORITHM.to_string());
}
if let Some(canonical) = SUPPORTED_TRANSIT_ALGORITHMS
.iter()
.find(|alg| trimmed.eq_ignore_ascii_case(alg))
{
return Ok((*canonical).to_string());
}
let supported = SUPPORTED_TRANSIT_ALGORITHMS.join(", ");
Err(vault_field_violation(
"algorithm",
format!("must be one of the supported transit algorithms: {supported}"),
format!("unsupported transit key algorithm '{trimmed}'; supported: {supported}"),
))
}
pub(crate) fn require_encryption_algorithm(algorithm: &str, operation: &str) -> Result<(), Status> {
if algorithm == SIGNING_TRANSIT_ALGORITHM {
return Err(vault_field_violation(
"key_name",
format!(
"must name an encryption key ({DEFAULT_TRANSIT_ALGORITHM}), not an \
{SIGNING_TRANSIT_ALGORITHM} signing key"
),
format!(
"{operation} requires an encryption key; the named key uses the \
{SIGNING_TRANSIT_ALGORITHM} signing algorithm (use Sign/Verify instead)"
),
));
}
Ok(())
}
pub(crate) fn require_signing_algorithm(algorithm: &str, operation: &str) -> Result<(), Status> {
if algorithm != SIGNING_TRANSIT_ALGORITHM {
return Err(vault_field_violation(
"key_name",
format!("must name an {SIGNING_TRANSIT_ALGORITHM} signing key"),
format!(
"{operation} requires an {SIGNING_TRANSIT_ALGORITHM} signing key; the named key \
uses the '{algorithm}' algorithm and has no public key to export"
),
));
}
Ok(())
}
pub(crate) fn transit_payload(ciphertext: &str) -> Result<&str, Status> {
parse_transit_envelope(ciphertext)
.map(|(_, encoded)| encoded)
.ok_or_else(|| {
vault_internal_status(
"get_secret_parse_envelope",
"stored secret has a malformed ciphertext envelope",
)
})
}