use crate::cipher::{self, NONCE_LEN};
use crate::codec;
use crate::container::{self, ContainerError, Header, VERSION};
use crate::dictionary::{Dictionary, DictionaryError};
use crate::kdf::{self, KdfParams, SALT_LEN};
use crate::antihacker;
use crate::oprf_net;
use crate::pqhybrid;
use crate::prelayers;
use crate::voprf;
const CIPHER_SUBKEY_INFO: &[u8] = b"quipu/v1/cipher";
pub struct Options<'a> {
pub pepper: &'a [u8],
pub kdf_params: KdfParams,
pub codebook_id: u16,
}
impl Default for Options<'_> {
fn default() -> Self {
Self {
pepper: b"",
kdf_params: KdfParams::default(),
codebook_id: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
Symbol(DictionaryError),
Container(ContainerError),
CodebookMismatch,
Decrypt,
}
pub fn encode(data: &[u8], passphrase: &str, dict: &Dictionary, opts: &Options) -> String {
let blob = encode_to_blob(data, passphrase, dict.fingerprint(), opts);
let indices = codec::encode_base_n(&blob, dict.base());
dict.encode(&indices)
.expect("los índices del codec están en [0, base)")
}
pub fn decode(
symbols: &str,
passphrase: &str,
dict: &Dictionary,
pepper: &[u8],
) -> Result<Vec<u8>, DecodeError> {
let indices = dict.decode(symbols).map_err(DecodeError::Symbol)?;
let blob = codec::decode_base_n(&indices, dict.base());
decode_from_blob(&blob, passphrase, dict.fingerprint(), pepper)
}
pub fn encode_to_blob(
data: &[u8],
passphrase: &str,
codebook_fingerprint: [u8; 8],
opts: &Options,
) -> Vec<u8> {
let mut salt = [0u8; SALT_LEN];
let mut nonce = [0u8; NONCE_LEN];
getrandom::getrandom(&mut salt).expect("RNG del sistema");
getrandom::getrandom(&mut nonce).expect("RNG del sistema");
let mut master = kdf::derive_master_key(passphrase, &salt, opts.pepper, &opts.kdf_params);
let mut cipher_key = kdf::derive_subkey(&master, CIPHER_SUBKEY_INFO);
antihacker::wipe(&mut master);
let header = Header {
version: VERSION,
flags: 0,
codebook_id: opts.codebook_id,
codebook_hash_prefix: codebook_fingerprint,
salt,
nonce,
kdf_mem_kib: opts.kdf_params.mem_kib,
kdf_iterations: opts.kdf_params.iterations,
kdf_parallelism: opts.kdf_params.parallelism,
};
let mut padded = prelayers::pad(data);
let aad = header.to_bytes();
let ciphertext = cipher::encrypt(&cipher_key, &nonce, &padded, &aad);
antihacker::wipe(&mut cipher_key);
antihacker::wipe(&mut padded);
container::serialize(&header, &ciphertext)
}
pub fn decode_from_blob(
blob: &[u8],
passphrase: &str,
expected_fingerprint: [u8; 8],
pepper: &[u8],
) -> Result<Vec<u8>, DecodeError> {
let (header, ciphertext) = container::parse(blob).map_err(DecodeError::Container)?;
if header.codebook_hash_prefix != expected_fingerprint {
return Err(DecodeError::CodebookMismatch);
}
let params = KdfParams {
mem_kib: header.kdf_mem_kib,
iterations: header.kdf_iterations,
parallelism: header.kdf_parallelism,
};
if !params.is_sane() {
return Err(DecodeError::Decrypt);
}
let mut master = kdf::derive_master_key(passphrase, &header.salt, pepper, ¶ms);
let mut cipher_key = kdf::derive_subkey(&master, CIPHER_SUBKEY_INFO);
antihacker::wipe(&mut master);
let aad = header.to_bytes();
let result = cipher::decrypt(&cipher_key, &header.nonce, ciphertext, &aad);
antihacker::wipe(&mut cipher_key);
let mut padded = result.map_err(|_| DecodeError::Decrypt)?;
let data = prelayers::unpad(&padded).map_err(|_| DecodeError::Decrypt);
antihacker::wipe(&mut padded); data
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OnlineError {
Network,
Denied,
Verification,
Decode(DecodeError),
}
fn harden(passphrase: &str, server_addr: &str, server_pub: &[u8; 32]) -> Result<[u8; 32], OnlineError> {
let (state, blinded) = voprf::blind(passphrase.as_bytes());
let resp = oprf_net::evaluate_remote_verified(server_addr, &blinded)
.map_err(|_| OnlineError::Network)?;
let (z, proof) = resp.ok_or(OnlineError::Denied)?;
voprf::finalize(passphrase.as_bytes(), &state, &z, &proof, server_pub)
.ok_or(OnlineError::Verification)
}
pub fn encode_online(
data: &[u8],
passphrase: &str,
server_addr: &str,
server_pub: &[u8; 32],
dict: &Dictionary,
opts: &Options,
) -> Result<String, OnlineError> {
let hardened = harden(passphrase, server_addr, server_pub)?;
let mut pepper = opts.pepper.to_vec();
pepper.extend_from_slice(&hardened);
let opts2 = Options {
pepper: &pepper,
kdf_params: opts.kdf_params,
codebook_id: opts.codebook_id,
};
Ok(encode(data, passphrase, dict, &opts2))
}
pub fn decode_online(
symbols: &str,
passphrase: &str,
server_addr: &str,
server_pub: &[u8; 32],
dict: &Dictionary,
base_pepper: &[u8],
) -> Result<Vec<u8>, OnlineError> {
let hardened = harden(passphrase, server_addr, server_pub)?;
let mut pepper = base_pepper.to_vec();
pepper.extend_from_slice(&hardened);
decode(symbols, passphrase, dict, &pepper).map_err(OnlineError::Decode)
}
pub fn encode_to_image(data: &[u8], passphrase: &str, opts: &Options) -> Vec<u8> {
let blob = encode_to_blob(data, passphrase, [0u8; 8], opts);
crate::render::bytes_to_png(&blob)
}
pub fn decode_from_image(
png: &[u8],
passphrase: &str,
pepper: &[u8],
) -> Result<Vec<u8>, DecodeError> {
let blob = crate::render::png_to_bytes(png)
.ok_or(DecodeError::Container(ContainerError::TooShort))?;
decode_from_blob(&blob, passphrase, [0u8; 8], pepper)
}
pub fn encode_to_robust_image(
data: &[u8],
passphrase: &str,
opts: &Options,
parity: u8,
) -> Vec<u8> {
let blob = encode_to_blob(data, passphrase, [0u8; 8], opts);
let protected = crate::ecc::protect(&blob, parity);
crate::render::bytes_to_png(&protected)
}
pub fn decode_from_robust_image(
png: &[u8],
passphrase: &str,
pepper: &[u8],
) -> Result<Vec<u8>, DecodeError> {
let protected = crate::render::png_to_bytes(png)
.ok_or(DecodeError::Container(ContainerError::TooShort))?;
let blob = crate::ecc::recover(&protected).ok_or(DecodeError::Decrypt)?;
decode_from_blob(&blob, passphrase, [0u8; 8], pepper)
}
pub fn encode_to_glyph_image(data: &[u8], passphrase: &str, opts: &Options) -> Vec<u8> {
let blob = encode_to_blob(data, passphrase, [0u8; 8], opts);
let font = crate::glyphfont::standard();
let indices = codec::encode_base_n(&blob, font.base());
font.render(&indices)
}
pub fn decode_from_glyph_image(
png: &[u8],
passphrase: &str,
pepper: &[u8],
) -> Result<Vec<u8>, DecodeError> {
let font = crate::glyphfont::standard();
let indices = font
.recognize(png)
.ok_or(DecodeError::Container(ContainerError::TooShort))?;
let blob = codec::decode_base_n(&indices, font.base());
decode_from_blob(&blob, passphrase, [0u8; 8], pepper)
}
const HYBRID_MAGIC: [u8; 4] = *b"QPQ1";
const HYBRID_VERSION: u8 = 1;
const HYBRID_PREFIX: usize = 4 + 1 + 1 + NONCE_LEN;
pub fn encode_to_recipient(
data: &[u8],
recipient: &pqhybrid::PublicKey,
dict: &Dictionary,
) -> String {
let (mut content_key, encapsulation) = pqhybrid::encapsulate(recipient);
let mut nonce = [0u8; NONCE_LEN];
getrandom::getrandom(&mut nonce).expect("RNG del sistema");
let mut header = Vec::with_capacity(HYBRID_PREFIX + encapsulation.len());
header.extend_from_slice(&HYBRID_MAGIC);
header.push(HYBRID_VERSION);
header.push(0u8); header.extend_from_slice(&nonce);
header.extend_from_slice(&encapsulation);
let mut padded = prelayers::pad(data);
let ciphertext = cipher::encrypt(&content_key, &nonce, &padded, &header);
antihacker::wipe(&mut content_key);
antihacker::wipe(&mut padded);
let mut blob = header;
blob.extend_from_slice(&ciphertext);
let indices = codec::encode_base_n(&blob, dict.base());
dict.encode(&indices)
.expect("los índices del codec están en [0, base)")
}
pub fn decode_as_recipient(
symbols: &str,
recipient: &pqhybrid::SecretKey,
dict: &Dictionary,
) -> Result<Vec<u8>, DecodeError> {
let indices = dict.decode(symbols).map_err(DecodeError::Symbol)?;
let blob = codec::decode_base_n(&indices, dict.base());
let header_len = HYBRID_PREFIX + pqhybrid::ENCAPSULATION_LEN;
if blob.len() < header_len {
return Err(DecodeError::Container(ContainerError::TooShort));
}
if blob[0..4] != HYBRID_MAGIC {
return Err(DecodeError::Container(ContainerError::BadMagic));
}
if blob[4] != HYBRID_VERSION {
return Err(DecodeError::Container(ContainerError::UnsupportedVersion(
blob[4],
)));
}
let nonce: [u8; NONCE_LEN] = blob[6..HYBRID_PREFIX].try_into().expect("24 bytes");
let encapsulation = &blob[HYBRID_PREFIX..header_len];
let aad = &blob[0..header_len];
let ciphertext = &blob[header_len..];
let mut content_key =
pqhybrid::decapsulate(recipient, encapsulation).ok_or(DecodeError::Decrypt)?;
let result = cipher::decrypt(&content_key, &nonce, ciphertext, aad);
antihacker::wipe(&mut content_key);
let mut padded = result.map_err(|_| DecodeError::Decrypt)?;
let data = prelayers::unpad(&padded).map_err(|_| DecodeError::Decrypt);
antihacker::wipe(&mut padded);
data
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn test_opts() -> Options<'static> {
Options {
pepper: b"",
kdf_params: KdfParams {
mem_kib: 64,
iterations: 1,
parallelism: 1,
},
codebook_id: 1,
}
}
fn ascii_dict() -> Dictionary {
Dictionary::new((0x21u8..=0x7e).map(|b| b as char).collect()).unwrap()
}
#[test]
fn round_trips_data() {
let dict = ascii_dict();
let data = b"mensaje secreto";
let symbols = encode(data, "clave-correcta", &dict, &test_opts());
let back = decode(&symbols, "clave-correcta", &dict, b"").unwrap();
assert_eq!(back, data);
}
#[test]
fn hides_length_within_padme_bucket() {
let dict = ascii_dict();
let a = encode(&[0u8; 100], "clave", &dict, &test_opts());
let b = encode(&[1u8; 101], "clave", &dict, &test_opts());
assert_eq!(a.chars().count(), b.chars().count());
}
#[test]
fn round_trips_empty_data() {
let dict = ascii_dict();
let symbols = encode(b"", "clave", &dict, &test_opts());
assert_eq!(decode(&symbols, "clave", &dict, b"").unwrap(), b"");
}
#[test]
fn wrong_passphrase_fails() {
let dict = ascii_dict();
let symbols = encode(b"datos", "correcta", &dict, &test_opts());
assert_eq!(
decode(&symbols, "incorrecta", &dict, b""),
Err(DecodeError::Decrypt)
);
}
#[test]
fn wrong_pepper_fails() {
let dict = ascii_dict();
let opts = Options {
pepper: b"pepper-correcto",
..test_opts()
};
let symbols = encode(b"datos", "clave", &dict, &opts);
assert_eq!(
decode(&symbols, "clave", &dict, b"pepper-incorrecto"),
Err(DecodeError::Decrypt)
);
}
#[test]
fn decode_rejects_malicious_kdf_params_without_panic() {
use crate::container::{self, Header, VERSION};
let dict = ascii_dict();
let header = Header {
version: VERSION,
flags: 0,
codebook_id: 1,
codebook_hash_prefix: dict.fingerprint(),
salt: [0u8; 16],
nonce: [0u8; 24],
kdf_mem_kib: u32::MAX,
kdf_iterations: u32::MAX,
kdf_parallelism: u32::MAX,
};
let blob = container::serialize(&header, b"ciphertext-falso-con-tag-relleno");
let indices = crate::codec::encode_base_n(&blob, dict.base());
let symbols = dict.encode(&indices).unwrap();
assert_eq!(
decode(&symbols, "clave", &dict, b""),
Err(DecodeError::Decrypt)
);
}
#[test]
fn tampered_symbols_fail() {
let dict = ascii_dict();
let symbols = encode(b"datos importantes", "clave", &dict, &test_opts());
let mut chars: Vec<char> = symbols.chars().collect();
chars[0] = if chars[0] == 'A' { 'B' } else { 'A' };
let tampered: String = chars.into_iter().collect();
assert!(decode(&tampered, "clave", &dict, b"").is_err());
}
proptest! {
#[test]
fn round_trips_any_data(
data in proptest::collection::vec(any::<u8>(), 0..128),
) {
let dict = ascii_dict();
let symbols = encode(&data, "clave", &dict, &test_opts());
let back = decode(&symbols, "clave", &dict, b"").unwrap();
prop_assert_eq!(back, data);
}
}
#[test]
fn image_channel_round_trips() {
let data = b"secreto representado como imagen";
let png = encode_to_image(data, "clave", &test_opts());
assert_eq!(
&png[0..8],
&[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]
);
assert_eq!(decode_from_image(&png, "clave", b"").unwrap(), data);
}
#[test]
fn image_wrong_passphrase_fails() {
let png = encode_to_image(b"x", "correcta", &test_opts());
assert!(decode_from_image(&png, "incorrecta", b"").is_err());
}
#[test]
fn glyph_image_round_trips() {
let data = b"secreto pintado con glifos IA nativos";
let png = encode_to_glyph_image(data, "clave", &test_opts());
assert_eq!(&png[0..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
assert_eq!(decode_from_glyph_image(&png, "clave", b"").unwrap(), data);
}
#[test]
fn robust_image_survives_channel_noise() {
let data = b"este mensaje sobrevive al ruido del canal impreso";
let png = encode_to_robust_image(data, "clave", &test_opts(), 16);
let mut payload = crate::render::png_to_bytes(&png).unwrap();
for byte in &mut payload[5..13] {
*byte ^= 0xFF;
}
let noisy = crate::render::bytes_to_png(&payload);
let recovered = decode_from_robust_image(&noisy, "clave", b"").unwrap();
assert_eq!(recovered, data);
}
fn spawn_voprf_server(
connections: usize,
allowed: bool,
) -> (String, [u8; 32], std::thread::JoinHandle<()>) {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = voprf::Server::new();
let pubkey = server.public_key();
let handle = std::thread::spawn(move || {
for _ in 0..connections {
let (mut stream, _) = listener.accept().unwrap();
crate::oprf_net::handle_connection_verified(&mut stream, &server, allowed).unwrap();
}
});
(addr, pubkey, handle)
}
#[test]
fn online_mode_round_trips_via_server() {
let (addr, pk, handle) = spawn_voprf_server(2, true); let dict = ascii_dict();
let data = b"secreto endurecido online";
let sym = encode_online(data, "clave", &addr, &pk, &dict, &test_opts()).unwrap();
let back = decode_online(&sym, "clave", &addr, &pk, &dict, b"").unwrap();
assert_eq!(back, data);
handle.join().unwrap();
}
#[test]
fn online_mode_denied_by_server_errors() {
let (addr, pk, handle) = spawn_voprf_server(1, false); let dict = ascii_dict();
let r = encode_online(b"x", "clave", &addr, &pk, &dict, &test_opts());
assert_eq!(r, Err(OnlineError::Denied));
handle.join().unwrap();
}
#[test]
fn online_mode_detects_dishonest_server() {
let (addr, _real_pk, handle) = spawn_voprf_server(1, true);
let wrong_pk = voprf::Server::new().public_key();
let dict = ascii_dict();
let r = encode_online(b"x", "clave", &addr, &wrong_pk, &dict, &test_opts());
assert_eq!(r, Err(OnlineError::Verification));
handle.join().unwrap();
}
#[test]
fn hybrid_round_trips_to_recipient() {
let (pk, sk) = pqhybrid::generate_keypair();
let dict = ascii_dict();
let data = b"secreto resistente a cuantica";
let symbols = encode_to_recipient(data, &pk, &dict);
assert_eq!(decode_as_recipient(&symbols, &sk, &dict).unwrap(), data);
}
#[test]
fn hybrid_wrong_recipient_fails() {
let (pk, _sk) = pqhybrid::generate_keypair();
let (_pk2, sk2) = pqhybrid::generate_keypair();
let dict = ascii_dict();
let symbols = encode_to_recipient(b"datos", &pk, &dict);
assert!(decode_as_recipient(&symbols, &sk2, &dict).is_err());
}
}