use crate::aead::AeadError;
use super::{CastError, CastValue};
pub enum EncryptedString {}
impl CastValue for EncryptedString {
type Value = String;
const SECRET: bool = true;
const KIND: crate::core::FieldCast = crate::core::FieldCast::Sealed;
fn to_db(value: &String) -> String {
encrypt(value.as_bytes()).unwrap_or_else(|e| {
panic!("encrypted cast write failed: {e}");
})
}
fn from_db(stored: &str) -> Result<String, CastError> {
let plaintext = decrypt(stored)?;
String::from_utf8(plaintext)
.map_err(|e| CastError(format!("decrypted bytes not UTF-8: {e}")))
}
}
fn cast_error(e: AeadError) -> CastError {
CastError(match e {
AeadError::MissingKey => {
"RUSTANGO_SECRET_KEY is not set (required for `EncryptedString` casts)".into()
}
AeadError::Encrypt(e) => format!("encrypt: {e}"),
AeadError::Decode(e) => format!("base64 decode: {e}"),
AeadError::Short => "ciphertext shorter than nonce".into(),
AeadError::Decrypt => "decryption failed (wrong key or tampered data)".into(),
})
}
pub(crate) fn encrypt(plaintext: &[u8]) -> Result<String, CastError> {
crate::aead::encrypt(plaintext, b"").map_err(cast_error)
}
fn decrypt(stored: &str) -> Result<Vec<u8>, CastError> {
crate::aead::decrypt(stored, b"").map_err(cast_error)
}
#[cfg(test)]
mod tests {
use super::super::{Cast, CastValue};
use super::EncryptedString;
fn with_key() {
crate::aead::set_test_key();
}
#[test]
fn round_trips_plaintext() {
with_key();
let stored = EncryptedString::to_db(&"hello world".to_owned());
assert_ne!(stored, "hello world", "stored form must be ciphertext");
let back = EncryptedString::from_db(&stored).unwrap();
assert_eq!(back, "hello world");
}
#[test]
fn distinct_nonce_per_encryption() {
with_key();
let a = EncryptedString::to_db(&"same".to_owned());
let b = EncryptedString::to_db(&"same".to_owned());
assert_ne!(a, b, "random nonce → ciphertext differs each write");
assert_eq!(EncryptedString::from_db(&a).unwrap(), "same");
assert_eq!(EncryptedString::from_db(&b).unwrap(), "same");
}
#[test]
fn the_previous_key_opens_old_values() {
let old = crate::aead::encrypt_with(Some("old-key"), b"client-secret", b"").unwrap();
let open = |cur, prev| crate::aead::decrypt_either(cur, prev, &old, b"");
assert_eq!(
open(Some("new-key"), Some("old-key")).unwrap(),
b"client-secret"
);
assert!(open(Some("new-key"), None).is_err());
assert!(open(Some("new-key"), Some("wrong")).is_err());
}
#[test]
fn the_cast_opens_a_value_under_the_previous_key() {
with_key();
let old = crate::aead::encrypt_with(Some("rotated-out"), b"client-secret", b"").unwrap();
assert!(EncryptedString::from_db(&old).is_err());
std::env::set_var(crate::aead::PREVIOUS_KEY_VAR, "rotated-out");
let back = EncryptedString::from_db(&old);
std::env::remove_var(crate::aead::PREVIOUS_KEY_VAR);
assert_eq!(back.unwrap(), "client-secret");
}
#[test]
fn tampered_ciphertext_fails() {
with_key();
let mut stored = EncryptedString::to_db(&"secret".to_owned());
stored.replace_range(30..31, if &stored[30..31] == "A" { "B" } else { "A" });
assert!(EncryptedString::from_db(&stored).is_err());
}
#[test]
fn cast_wrapper_into_sqlvalue_is_ciphertext_string() {
with_key();
let c: Cast<EncryptedString> = Cast::new("x".to_owned());
let v: crate::core::SqlValue = c.into();
match v {
crate::core::SqlValue::String(s) => assert_ne!(s, "x"),
other => panic!("expected SqlValue::String, got {other:?}"),
}
}
}