runtime: normalize structured secret errors
This commit is contained in:
@@ -32,6 +32,7 @@ impl SecretCrypto {
|
||||
let trimmed = master_key.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(RuntimeError::SecretCrypto {
|
||||
operation: "initialize secret crypto",
|
||||
details: "CRANK_MASTER_KEY must not be empty".to_owned(),
|
||||
});
|
||||
}
|
||||
@@ -54,16 +55,19 @@ impl SecretCrypto {
|
||||
pub fn decrypt(&self, key_version: &str, ciphertext: &str) -> Result<Value, RuntimeError> {
|
||||
let envelope: CipherEnvelope =
|
||||
serde_json::from_str(ciphertext).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "decode secret envelope",
|
||||
details: format!("failed to decode secret envelope: {error}"),
|
||||
})?;
|
||||
let nonce_bytes =
|
||||
STANDARD
|
||||
.decode(envelope.nonce_b64)
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "decode secret nonce",
|
||||
details: format!("failed to decode secret nonce: {error}"),
|
||||
})?;
|
||||
let ciphertext_bytes = STANDARD.decode(envelope.ciphertext_b64).map_err(|error| {
|
||||
RuntimeError::SecretCrypto {
|
||||
operation: "decode secret payload",
|
||||
details: format!("failed to decode secret payload: {error}"),
|
||||
}
|
||||
})?;
|
||||
@@ -71,10 +75,12 @@ impl SecretCrypto {
|
||||
let plaintext = cipher
|
||||
.decrypt(Nonce::from_slice(&nonce_bytes), ciphertext_bytes.as_ref())
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "decrypt secret value",
|
||||
details: format!("failed to decrypt secret value: {error}"),
|
||||
})?;
|
||||
|
||||
serde_json::from_slice(&plaintext).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "deserialize secret value",
|
||||
details: format!("failed to deserialize secret value: {error}"),
|
||||
})
|
||||
}
|
||||
@@ -84,6 +90,7 @@ impl SecretCrypto {
|
||||
LEGACY_KEY_VERSION => Ok(&self.legacy_cipher),
|
||||
CURRENT_KEY_VERSION => Ok(&self.current_cipher),
|
||||
other => Err(RuntimeError::SecretCrypto {
|
||||
operation: "select secret key version",
|
||||
details: format!("unsupported secret key version: {other}"),
|
||||
}),
|
||||
}
|
||||
@@ -93,6 +100,7 @@ impl SecretCrypto {
|
||||
fn derive_legacy_cipher(master_key: &str) -> Result<Aes256Gcm, RuntimeError> {
|
||||
let digest = Sha256::digest(master_key.as_bytes());
|
||||
Aes256Gcm::new_from_slice(digest.as_slice()).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "initialize legacy secret crypto",
|
||||
details: format!("failed to initialize legacy secret crypto: {error}"),
|
||||
})
|
||||
}
|
||||
@@ -102,9 +110,11 @@ fn derive_hkdf_cipher(master_key: &str) -> Result<Aes256Gcm, RuntimeError> {
|
||||
let mut key_bytes = [0_u8; 32];
|
||||
hkdf.expand(SECRET_ENVELOPE_INFO, &mut key_bytes)
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "derive secret key with hkdf",
|
||||
details: format!("failed to derive secret key with HKDF: {error}"),
|
||||
})?;
|
||||
Aes256Gcm::new_from_slice(&key_bytes).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "initialize secret crypto",
|
||||
details: format!("failed to initialize secret crypto: {error}"),
|
||||
})
|
||||
}
|
||||
@@ -115,6 +125,7 @@ fn encrypt_value_with_cipher(
|
||||
action: &str,
|
||||
) -> Result<String, RuntimeError> {
|
||||
let plaintext = serde_json::to_vec(value).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "serialize secret value",
|
||||
details: format!("failed to serialize secret value: {error}"),
|
||||
})?;
|
||||
let mut nonce_bytes = [0_u8; 12];
|
||||
@@ -124,6 +135,7 @@ fn encrypt_value_with_cipher(
|
||||
cipher
|
||||
.encrypt(nonce, plaintext.as_ref())
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "encrypt secret value",
|
||||
details: format!("failed to {action}: {error}"),
|
||||
})?;
|
||||
let envelope = CipherEnvelope {
|
||||
@@ -132,6 +144,7 @@ fn encrypt_value_with_cipher(
|
||||
};
|
||||
|
||||
serde_json::to_string(&envelope).map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "encode secret ciphertext",
|
||||
details: format!("failed to encode secret ciphertext: {error}"),
|
||||
})
|
||||
}
|
||||
@@ -148,6 +161,7 @@ fn current_key_bytes(master_key: &str) -> Result<[u8; 32], RuntimeError> {
|
||||
let mut key_bytes = [0_u8; 32];
|
||||
hkdf.expand(SECRET_ENVELOPE_INFO, &mut key_bytes)
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "derive secret key with hkdf",
|
||||
details: format!("failed to derive secret key with HKDF: {error}"),
|
||||
})?;
|
||||
Ok(key_bytes)
|
||||
@@ -163,6 +177,7 @@ fn legacy_key_bytes(master_key: &str) -> [u8; 32] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::RuntimeError;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
@@ -202,11 +217,13 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_empty_master_key() {
|
||||
let error = SecretCrypto::new(" ").err().unwrap();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("CRANK_MASTER_KEY must not be empty")
|
||||
);
|
||||
match error {
|
||||
RuntimeError::SecretCrypto { operation, details } => {
|
||||
assert_eq!(operation, "initialize secret crypto");
|
||||
assert_eq!(details, "CRANK_MASTER_KEY must not be empty");
|
||||
}
|
||||
other => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -246,6 +263,12 @@ mod tests {
|
||||
let ciphertext = crypto.encrypt(&json!({"token": "top-secret"})).unwrap();
|
||||
let error = crypto.decrypt("v999", &ciphertext).unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("unsupported secret key version"));
|
||||
match error {
|
||||
RuntimeError::SecretCrypto { operation, details } => {
|
||||
assert_eq!(operation, "select secret key version");
|
||||
assert!(details.contains("unsupported secret key version"));
|
||||
}
|
||||
other => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user