use std::fmt;
use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use serde_json::{Map, Value};
use uuid::Uuid;
pub use zeroize::Zeroizing;
use super::emit::{Node, Obj, render};
use super::{Date, Vcon, WriteError, gzip, is_https};
use crate::Form;
use crate::form::form_of;
use crate::gzip::inflate;
use crate::identity::{parse_uuid_text, unsigned_identity};
use crate::json::parse_object;
use crate::read::{Limits, MAX_SIGNED_LAYERS};
const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Compression {
None,
Gzip,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Chain {
X5c(Vec<Vec<u8>>),
X5u(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Sealed {
iv: Vec<u8>,
ciphertext: Vec<u8>,
tag: Vec<u8>,
}
impl Sealed {
#[must_use]
pub fn new(iv: Vec<u8>, ciphertext: Vec<u8>, tag: Vec<u8>) -> Sealed {
Sealed {
iv,
ciphertext,
tag,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[must_use]
pub struct CryptoError(String);
impl CryptoError {
pub fn new(message: impl Into<String>) -> CryptoError {
CryptoError(message.into())
}
}
impl fmt::Display for CryptoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for CryptoError {}
pub trait Signer {
fn alg(&self) -> &str;
fn chain(&self) -> &Chain;
fn sign(&self, input: &[u8]) -> Result<Vec<u8>, CryptoError>;
}
pub trait KeyEncrypter {
fn alg(&self) -> &str;
fn encrypt_key(&self, cek: &[u8]) -> Result<Vec<u8>, CryptoError>;
}
pub trait ContentEncrypter {
fn enc(&self) -> &str;
fn new_key(&self) -> Result<Zeroizing<Vec<u8>>, CryptoError>;
fn seal(&self, cek: &[u8], plaintext: &[u8], aad: &[u8]) -> Result<Sealed, CryptoError>;
}
pub fn sign(
vcon: &Vcon,
signed_at: Date,
compression: Compression,
signers: &[&dyn Signer],
) -> Result<Vec<u8>, WriteError> {
check_signers(signers)?;
let mut signed = vcon.clone();
signed.set_updated_at(signed_at);
let payload = packed(signed.to_json()?, compression);
jws(&payload, vcon.uuid(), signers)
}
pub fn sign_again(
signed: &[u8],
compression: Compression,
signer: &dyn Signer,
limits: Limits,
) -> Result<Vec<u8>, WriteError> {
let inner = read_signed(signed, limits)?;
if inner.layers >= MAX_SIGNED_LAYERS {
return Err(WriteError::TooManyLayers);
}
check_signers(&[signer])?;
let payload = packed(signed.to_vec(), compression);
jws(&payload, inner.uuid, &[signer])
}
pub fn encrypt(
signed: &[u8],
compression: Compression,
content: &dyn ContentEncrypter,
recipients: &[&dyn KeyEncrypter],
limits: Limits,
) -> Result<Vec<u8>, WriteError> {
let inner = read_signed(signed, limits)?;
if recipients.is_empty() {
return Err(WriteError::NoRecipient);
}
let plaintext = packed(signed.to_vec(), compression);
let cek = content.new_key().map_err(WriteError::Crypto)?;
let sealed = content
.seal(&cek, &plaintext.0, b"")
.map_err(WriteError::Crypto)?;
let mut wrapped = Vec::new();
for recipient in recipients {
let key = recipient.encrypt_key(&cek).map_err(WriteError::Crypto)?;
let mut header = Obj::default();
header.put("alg", Node::str(recipient.alg()));
let mut entry = Obj::default();
entry.put("header", header.done());
entry.put("encrypted_key", Node::Str(URL_SAFE_NO_PAD.encode(key)));
wrapped.push(entry.done());
}
let mut unprotected = Obj::default();
unprotected.put("cty", Node::str(plaintext.cty()));
unprotected.put("enc", Node::str(content.enc()));
unprotected.put("uuid", Node::str(&inner.uuid.hyphenated().to_string()));
let mut top = Obj::default();
top.put("unprotected", unprotected.done());
top.put("recipients", Node::Arr(wrapped));
top.put("iv", Node::Str(URL_SAFE_NO_PAD.encode(&sealed.iv)));
top.put(
"ciphertext",
Node::Str(URL_SAFE_NO_PAD.encode(&sealed.ciphertext)),
);
top.put("tag", Node::Str(URL_SAFE_NO_PAD.encode(&sealed.tag)));
render(&top.done())
}
struct Packed(Vec<u8>);
impl Packed {
fn cty(&self) -> &'static str {
if self.0.starts_with(&GZIP_MAGIC) {
"application/vcon+gzip"
} else {
"application/vcon"
}
}
}
fn packed(bytes: Vec<u8>, compression: Compression) -> Packed {
match compression {
Compression::None => Packed(bytes),
Compression::Gzip => Packed(gzip(&bytes)),
}
}
fn check_signers(signers: &[&dyn Signer]) -> Result<(), WriteError> {
if signers.is_empty() {
return Err(WriteError::NoSigner);
}
for signer in signers {
match signer.chain() {
Chain::X5c(certificates) if certificates.is_empty() => {
return Err(WriteError::BadChain);
}
Chain::X5u(url) if !is_https(url) => return Err(WriteError::BadChain),
_ => {}
}
}
Ok(())
}
fn jws(payload: &Packed, uuid: Uuid, signers: &[&dyn Signer]) -> Result<Vec<u8>, WriteError> {
let mut cty = Obj::default();
cty.put("cty", Node::str(payload.cty()));
let protected = URL_SAFE_NO_PAD.encode(render(&cty.done())?);
let encoded = URL_SAFE_NO_PAD.encode(&payload.0);
let input = format!("{protected}.{encoded}");
let mut signatures = Vec::new();
for signer in signers {
let signature = signer.sign(input.as_bytes()).map_err(WriteError::Crypto)?;
let mut header = Obj::default();
header.put("alg", Node::str(signer.alg()));
match signer.chain() {
Chain::X5c(certificates) => header.put(
"x5c",
Node::Arr(
certificates
.iter()
.map(|der| Node::Str(STANDARD.encode(der)))
.collect(),
),
),
Chain::X5u(url) => header.put("x5u", Node::str(url)),
}
header.put("uuid", Node::str(&uuid.hyphenated().to_string()));
let mut entry = Obj::default();
entry.put("protected", Node::str(&protected));
entry.put("header", header.done());
entry.put("signature", Node::Str(URL_SAFE_NO_PAD.encode(signature)));
signatures.push(entry.done());
}
let mut top = Obj::default();
top.put("payload", Node::Str(encoded));
top.put("signatures", Node::Arr(signatures));
render(&top.done())
}
struct Inner {
uuid: Uuid,
layers: usize,
}
fn read_signed(signed: &[u8], limits: Limits) -> Result<Inner, WriteError> {
let not_signed = |_| WriteError::NotSigned;
let mut budget = limits.inflated;
let mut object = open(signed, &mut budget).map_err(not_signed)?;
if form_of(&object).map_err(not_signed)? != Form::Signed {
return Err(WriteError::NotSigned);
}
let mut header_uuids = Vec::new();
let mut layers = 0;
loop {
layers += 1;
if layers > MAX_SIGNED_LAYERS {
return Err(WriteError::NotSigned);
}
header_uuids.extend(header_uuids_of(&object)?);
let Some(Value::String(payload)) = object.get("payload") else {
return Err(WriteError::NotSigned);
};
let bytes = URL_SAFE_NO_PAD
.decode(payload)
.map_err(|_| WriteError::NotSigned)?;
object = open(&bytes, &mut budget).map_err(not_signed)?;
match form_of(&object).map_err(not_signed)? {
Form::Signed => {}
Form::Unsigned => break,
_ => return Err(WriteError::NotSigned),
}
}
let uuid = unsigned_identity(&object).map_err(not_signed)?;
for header_uuid in header_uuids {
match header_uuid {
Value::String(text) if parse_uuid_text(&text) == Ok(uuid) => {}
_ => return Err(WriteError::NotSigned),
}
}
Ok(Inner { uuid, layers })
}
fn open(bytes: &[u8], budget: &mut u64) -> Result<Map<String, Value>, crate::read::Rejection> {
let (inner, was_gzip) = inflate(bytes, None, *budget)?;
if was_gzip {
*budget = budget.saturating_sub(inner.len() as u64);
}
parse_object(&inner)
}
fn header_uuids_of(object: &Map<String, Value>) -> Result<Vec<Value>, WriteError> {
let Some(Value::Array(signatures)) = object.get("signatures") else {
return Err(WriteError::NotSigned);
};
let mut uuids = Vec::new();
for signature in signatures {
match signature.get("header") {
None => {}
Some(Value::Object(header)) => uuids.extend(header.get("uuid").cloned()),
Some(_) => return Err(WriteError::NotSigned),
}
}
Ok(uuids)
}