philbin 1.0.1

A pure Rust AEGIS library with SIMD and runtime CPU detection
Documentation
use anyhow::Context;
use philbin::{
  careful::Nonce256,
  easy::{AssociatedData, AuthTag256, Ciphertext, Key256, Plaintext},
};
use test_utils::AegisTestCase;
use rstest::rstest;
use similar_asserts::assert_eq;

// Extracts the nonce from a (nonce || ciphertext || tag) slice.
// `nonce` and `tag` are both 256 bits wide.
fn extract_nonce(data: &[u8]) -> anyhow::Result<Nonce256> {
  let nonce_array = data
    .split_at(Nonce256::BYTES)
    .0
    .as_array()
    .context("data must contain a 32 byte nonce")?;

  Ok(Nonce256::new(*nonce_array)?)
}

// Extracts (ciphertext || tag) from a (nonce || ciphertext || tag) slice.
// `nonce` and `tag` are both 256 bits wide.
fn extract_ciphertext_and_tag(data: &[u8]) -> &[u8] {
  data.split_at(Nonce256::BYTES).1
}

// Returns a byte vec holding (nonce || ciphertext || tag).
// `nonce` and `tag` are both 256 bits wide.
fn assemble_encrypted_payload(test_case: &AegisTestCase) -> Vec<u8> {
  let mut out = vec![
    0u8;
    test_case.nonce.len()
      + test_case.ciphertext.len()
      + test_case.tag256.len()
  ];
  out[..Nonce256::BYTES].copy_from_slice(&test_case.nonce);
  out[Nonce256::BYTES..Nonce256::BYTES + test_case.ciphertext.len()]
    .copy_from_slice(&test_case.ciphertext);
  out[Nonce256::BYTES + test_case.ciphertext.len()..]
    .copy_from_slice(&test_case.tag256);
  out
}

#[rstest]
fn roundtrip() -> anyhow::Result<()> {
  let plaintext = b"foo bar zoo";
  let associated_data = b"moo goo";
  let key = Key256::generate()?;
  let encrypted_payload = philbin::easy::encrypt(
    Plaintext::new(plaintext),
    AssociatedData::new(associated_data),
    &key,
  )?;

  let decrypted_plaintext = philbin::easy::decrypt(
    Ciphertext::new(&encrypted_payload),
    AssociatedData::new(associated_data),
    &key,
  )?;

  assert_eq!(expected: plaintext.as_slice(), actual: decrypted_plaintext);

  Ok(())
}

#[rstest]
fn encrypt_basic() -> anyhow::Result<()> {
  let test_case = test_utils::aegis256x4::example();

  let encrypted_payload = philbin::easy::encrypt(
    Plaintext::new(&test_case.plaintext),
    AssociatedData::new(&test_case.associated_data),
    &Key256::from_bytes(&test_case.key)?,
  )?;

  // The nonce is always different so we just assert it's not all-zero
  assert_ne!(
    [0; Nonce256::BYTES],
    extract_nonce(&encrypted_payload)?.into_array(),
    "nonce is all-zero"
  );

  // NOTE: We cannot compare with expected outputs in `test_case` because those
  // assume a specific nonce (the one in `test_case.nonce`) and we can't provide
  // a nonce to `philbin::easy::encrypt` (that's the whole point). So we compare
  // the outputs with what aegise256x4 in `careful` produces. Since that
  // has separate tests confirming it is correct, `philbin::easy::encrypt` is
  // transitively proven to be correct.

  let ciphertext_and_tag = philbin::careful::aegis256x4::encrypt::<AuthTag256>(
    Plaintext::new(&test_case.plaintext),
    AssociatedData::new(&test_case.associated_data),
    &Key256::from_bytes(&test_case.key)?,
    extract_nonce(&encrypted_payload)?,
  )?;

  assert_eq!(
    expected: ciphertext_and_tag,
    actual: extract_ciphertext_and_tag(&encrypted_payload),
    "(ciphertext || tag) is wrong");

  Ok(())
}

#[rstest]
fn decrypt_basic() -> anyhow::Result<()> {
  let test_case = test_utils::aegis256x4::example();
  let plaintext = philbin::easy::decrypt(
    Ciphertext::new(&assemble_encrypted_payload(&test_case)),
    AssociatedData::new(&test_case.associated_data),
    &Key256::from_bytes(&test_case.key)?,
  )?;

  assert_eq!(expected: test_case.plaintext, actual: plaintext);

  Ok(())
}