use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
pub const ENVELOPE_VERSION: u8 = 0x01;
pub const NONCE_LENGTH: usize = 12;
pub const KEY_LENGTH: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclaredType {
String,
Integer,
Float,
Boolean,
Json,
BlobRef,
Bytes,
}
impl DeclaredType {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"string" => Some(DeclaredType::String),
"integer" => Some(DeclaredType::Integer),
"float" => Some(DeclaredType::Float),
"boolean" => Some(DeclaredType::Boolean),
"json" => Some(DeclaredType::Json),
"blob_ref" => Some(DeclaredType::BlobRef),
"bytes" => Some(DeclaredType::Bytes),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PlainValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Json(String),
BlobRef(String),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct DecryptError(pub String);
impl std::fmt::Display for DecryptError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "client.decrypt_failed: {}", self.0)
}
}
impl std::error::Error for DecryptError {}
pub fn serialize_plain(value: &PlainValue) -> Result<Vec<u8>, String> {
Ok(match value {
PlainValue::String(s) | PlainValue::Json(s) | PlainValue::BlobRef(s) => {
s.as_bytes().to_vec()
}
PlainValue::Integer(i) => i.to_le_bytes().to_vec(),
PlainValue::Float(f) => f.to_le_bytes().to_vec(),
PlainValue::Boolean(b) => vec![if *b { 1 } else { 0 }],
PlainValue::Bytes(b) => b.clone(),
})
}
pub fn deserialize_plain(declared: DeclaredType, bytes: &[u8]) -> Result<PlainValue, DecryptError> {
match declared {
DeclaredType::String | DeclaredType::Json | DeclaredType::BlobRef => {
let text = std::str::from_utf8(bytes)
.map_err(|_| DecryptError("decrypted value is not valid UTF-8".to_owned()))?
.to_owned();
if declared == DeclaredType::Json {
serde_json::from_str::<serde_json::Value>(&text)
.map_err(|_| DecryptError("decrypted json does not parse".to_owned()))?;
Ok(PlainValue::Json(text))
} else if declared == DeclaredType::BlobRef {
Ok(PlainValue::BlobRef(text))
} else {
Ok(PlainValue::String(text))
}
}
DeclaredType::Integer => {
if bytes.len() != 8 {
return Err(DecryptError("decrypted integer must be 8 bytes".to_owned()));
}
let mut a = [0u8; 8];
a.copy_from_slice(bytes);
Ok(PlainValue::Integer(i64::from_le_bytes(a)))
}
DeclaredType::Float => {
if bytes.len() != 8 {
return Err(DecryptError("decrypted float must be 8 bytes".to_owned()));
}
let mut a = [0u8; 8];
a.copy_from_slice(bytes);
Ok(PlainValue::Float(f64::from_le_bytes(a)))
}
DeclaredType::Boolean => {
if bytes.len() != 1 || (bytes[0] != 0 && bytes[0] != 1) {
return Err(DecryptError(
"decrypted boolean must be one 0x00/0x01 byte".to_owned(),
));
}
Ok(PlainValue::Boolean(bytes[0] == 1))
}
DeclaredType::Bytes => Ok(PlainValue::Bytes(bytes.to_vec())),
}
}
pub struct Envelope {
pub key_id: String,
pub nonce: [u8; NONCE_LENGTH],
pub ciphertext: Vec<u8>,
}
pub fn encode_envelope(env: &Envelope) -> Result<Vec<u8>, String> {
let key_id_bytes = env.key_id.as_bytes();
if key_id_bytes.len() > 0xff {
return Err("keyId exceeds 255 UTF-8 bytes".to_owned());
}
let mut out = Vec::with_capacity(2 + key_id_bytes.len() + NONCE_LENGTH + env.ciphertext.len());
out.push(ENVELOPE_VERSION);
out.push(key_id_bytes.len() as u8);
out.extend_from_slice(key_id_bytes);
out.extend_from_slice(&env.nonce);
out.extend_from_slice(&env.ciphertext);
Ok(out)
}
pub fn decode_envelope(bytes: &[u8]) -> Result<Envelope, DecryptError> {
if bytes.len() < 2 {
return Err(DecryptError("envelope truncated".to_owned()));
}
if bytes[0] != ENVELOPE_VERSION {
return Err(DecryptError(format!(
"unknown envelope version 0x{:02x}",
bytes[0]
)));
}
let key_id_len = bytes[1] as usize;
let key_id_end = 2 + key_id_len;
let nonce_end = key_id_end + NONCE_LENGTH;
if bytes.len() < nonce_end + 16 {
return Err(DecryptError("envelope truncated".to_owned()));
}
let key_id = std::str::from_utf8(&bytes[2..key_id_end])
.map_err(|_| DecryptError("keyId is not valid UTF-8".to_owned()))?
.to_owned();
let mut nonce = [0u8; NONCE_LENGTH];
nonce.copy_from_slice(&bytes[key_id_end..nonce_end]);
Ok(Envelope {
key_id,
nonce,
ciphertext: bytes[nonce_end..].to_vec(),
})
}
fn cipher(key: &[u8]) -> Result<Aes256Gcm, String> {
if key.len() != KEY_LENGTH {
return Err(format!("AES-256-GCM key must be {KEY_LENGTH} bytes"));
}
Aes256Gcm::new_from_slice(key).map_err(|e| format!("bad key: {e}"))
}
pub fn encrypt_value(
value: &PlainValue,
key_id: &str,
key: &[u8],
nonce: [u8; NONCE_LENGTH],
) -> Result<Vec<u8>, String> {
let plaintext = serialize_plain(value)?;
let ct = cipher(key)?
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: &plaintext,
aad: &[],
},
)
.map_err(|e| format!("aes-gcm encrypt failed: {e}"))?;
encode_envelope(&Envelope {
key_id: key_id.to_owned(),
nonce,
ciphertext: ct,
})
}
pub fn decrypt_value(
declared: DeclaredType,
envelope_bytes: &[u8],
key_provider: impl Fn(&str) -> Option<Vec<u8>>,
) -> Result<PlainValue, DecryptError> {
let env = decode_envelope(envelope_bytes)?;
let key = key_provider(&env.key_id)
.ok_or_else(|| DecryptError(format!("no key for keyId {:?}", env.key_id)))?;
let c = cipher(&key).map_err(DecryptError)?;
let plaintext = c
.decrypt(
Nonce::from_slice(&env.nonce),
Payload {
msg: &env.ciphertext,
aad: &[],
},
)
.map_err(|_| {
DecryptError(format!(
"GCM authentication failed for keyId {:?} (wrong key or corrupt ciphertext)",
env.key_id
))
})?;
deserialize_plain(declared, &plaintext)
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: [u8; 32] = [7u8; 32];
const NONCE: [u8; 12] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
fn provider(id: &str) -> Option<Vec<u8>> {
if id == "k1" {
Some(KEY.to_vec())
} else {
None
}
}
#[test]
fn round_trips_every_declared_type() {
let cases = vec![
(
DeclaredType::String,
PlainValue::String("hello 🔐".to_owned()),
),
(DeclaredType::Json, PlainValue::Json("{\"a\":1}".to_owned())),
(
DeclaredType::BlobRef,
PlainValue::BlobRef("{\"blobId\":\"x\"}".to_owned()),
),
(DeclaredType::Integer, PlainValue::Integer(9007199254740991)),
(
DeclaredType::Integer,
PlainValue::Integer(-9007199254740991),
),
(DeclaredType::Float, PlainValue::Float(3.141592653589793)),
(DeclaredType::Boolean, PlainValue::Boolean(true)),
(DeclaredType::Bytes, PlainValue::Bytes(vec![0xde, 0xad])),
];
for (ty, value) in cases {
let env = encrypt_value(&value, "k1", &KEY, NONCE).unwrap();
assert_eq!(env[0], ENVELOPE_VERSION);
let back = decrypt_value(ty, &env, provider).unwrap();
assert_eq!(back, value);
}
}
#[test]
fn wrong_key_fails() {
let env = encrypt_value(&PlainValue::String("s".to_owned()), "k1", &KEY, NONCE).unwrap();
let bad = decrypt_value(DeclaredType::String, &env, |id| {
if id == "k1" {
Some(vec![9u8; 32])
} else {
None
}
});
assert!(bad.is_err());
}
#[test]
fn fixed_nonce_is_deterministic() {
let a = encrypt_value(&PlainValue::String("x".to_owned()), "k1", &KEY, NONCE).unwrap();
let b = encrypt_value(&PlainValue::String("x".to_owned()), "k1", &KEY, NONCE).unwrap();
assert_eq!(a, b);
}
}