#![allow(dead_code)]
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use anyhow::{anyhow, bail, Result};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use std::io::{Read, Write};
const MAGIC: &[u8; 4] = b"DOVE";
const VERSION: u8 = 1;
pub const DEFAULT_CHUNK: usize = 1 << 20;
pub fn gen_key() -> [u8; 32] {
let mut k = [0u8; 32];
getrandom::getrandom(&mut k).expect("OS RNG unavailable");
k
}
pub fn key_to_fragment(key: &[u8; 32]) -> String {
URL_SAFE_NO_PAD.encode(key)
}
pub fn key_from_fragment(s: &str) -> Result<[u8; 32]> {
let bytes = URL_SAFE_NO_PAD
.decode(s.trim())
.map_err(|_| anyhow!("invalid key in the link"))?;
bytes
.try_into()
.map_err(|_| anyhow!("key in the link is the wrong length"))
}
pub const PBKDF2_ITERS: u32 = 600_000;
pub fn derive_key(pin: &str, fragment_secret: &[u8; 32]) -> [u8; 32] {
pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(pin.as_bytes(), fragment_secret, PBKDF2_ITERS)
}
pub fn pin_hash(share_id: &str, pin: &str) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(share_id.as_bytes());
h.update(b":");
h.update(pin.as_bytes());
hex_lower(&h.finalize())
}
pub fn meta_key(secret: &[u8; 32]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(secret);
h.update(b"dove-meta-v1");
h.finalize().into()
}
pub fn encrypt_meta(secret: &[u8; 32], plaintext: &[u8]) -> String {
let key = meta_key(secret);
let cipher = Aes256Gcm::new_from_slice(&key).expect("32-byte key");
let mut nonce = [0u8; 12];
getrandom::getrandom(&mut nonce).expect("OS RNG unavailable");
let ct = cipher
.encrypt(Nonce::from_slice(&nonce), plaintext)
.expect("metadata encryption");
let mut blob = nonce.to_vec();
blob.extend_from_slice(&ct);
URL_SAFE_NO_PAD.encode(blob)
}
pub fn decrypt_meta(secret: &[u8; 32], blob_b64: &str) -> Result<Vec<u8>> {
let blob = URL_SAFE_NO_PAD
.decode(blob_b64.trim())
.map_err(|_| anyhow!("invalid metadata in the link"))?;
if blob.len() < 12 + 16 {
bail!("metadata in the link is too short");
}
let key = meta_key(secret);
let cipher = Aes256Gcm::new_from_slice(&key).expect("32-byte key");
let (nonce, ct) = blob.split_at(12);
cipher
.decrypt(Nonce::from_slice(nonce), ct)
.map_err(|_| anyhow!("metadata decryption failed"))
}
pub fn gen_gate_secret() -> String {
let mut b = [0u8; 32];
getrandom::getrandom(&mut b).expect("OS RNG unavailable");
hex_lower(&b)
}
pub fn mint_share_id(gate_secret: &[u8], nonce: &[u8; 8]) -> String {
use hmac::{Hmac, Mac};
let mut m =
<Hmac<sha2::Sha256> as Mac>::new_from_slice(gate_secret).expect("HMAC accepts any key len");
m.update(nonce);
let tag = m.finalize().into_bytes();
let mut raw = nonce.to_vec();
raw.extend_from_slice(&tag[..8]);
hex_lower(&raw)
}
pub fn new_share_id(gate_secret_hex: &str) -> Result<String> {
let secret = hex_decode(gate_secret_hex).ok_or_else(|| anyhow!("invalid gate secret"))?;
let mut nonce = [0u8; 8];
getrandom::getrandom(&mut nonce).expect("OS RNG unavailable");
Ok(mint_share_id(&secret, &nonce))
}
pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
s.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
}
s
}
pub fn encrypt<R: Read, W: Write>(
key: &[u8; 32],
chunk_size: usize,
mut reader: R,
mut writer: W,
) -> Result<()> {
let cipher = Aes256Gcm::new_from_slice(key).expect("32-byte key");
let mut prefix = [0u8; 8];
getrandom::getrandom(&mut prefix).expect("OS RNG unavailable");
writer.write_all(MAGIC)?;
writer.write_all(&[VERSION])?;
writer.write_all(&prefix)?;
writer.write_all(&(chunk_size as u32).to_be_bytes())?;
let mut buf = vec![0u8; chunk_size];
let mut counter: u32 = 0;
loop {
let n = read_full(&mut reader, &mut buf)?;
let is_last = n < chunk_size; let ct = cipher
.encrypt(
&nonce(&prefix, counter),
Payload {
msg: &buf[..n],
aad: &aad(counter, is_last),
},
)
.map_err(|_| anyhow!("encryption failed"))?;
writer.write_all(&[is_last as u8])?;
writer.write_all(&(ct.len() as u32).to_be_bytes())?;
writer.write_all(&ct)?;
if is_last {
break;
}
counter += 1;
}
Ok(())
}
pub fn decrypt<R: Read, W: Write>(key: &[u8; 32], mut reader: R, mut writer: W) -> Result<()> {
let cipher = Aes256Gcm::new_from_slice(key).expect("32-byte key");
let mut magic = [0u8; 4];
reader
.read_exact(&mut magic)
.map_err(|_| anyhow!("not a dove-encrypted file"))?;
if &magic != MAGIC {
bail!("not a dove-encrypted file");
}
let mut ver = [0u8; 1];
reader.read_exact(&mut ver)?;
if ver[0] != VERSION {
bail!("unsupported dove format version {}", ver[0]);
}
let mut prefix = [0u8; 8];
reader.read_exact(&mut prefix)?;
let mut _chunk_size = [0u8; 4];
reader.read_exact(&mut _chunk_size)?;
let mut counter: u32 = 0;
loop {
let mut flag = [0u8; 1];
if reader.read(&mut flag)? == 0 {
bail!("truncated: the file ended before its final chunk");
}
let is_last = flag[0];
let mut len = [0u8; 4];
reader.read_exact(&mut len)?;
let mut ct = vec![0u8; u32::from_be_bytes(len) as usize];
reader.read_exact(&mut ct)?;
let pt = cipher
.decrypt(
&nonce(&prefix, counter),
Payload {
msg: &ct,
aad: &aad(counter, is_last == 1),
},
)
.map_err(|_| anyhow!("decryption failed — wrong key, or the data was tampered with"))?;
writer.write_all(&pt)?;
if is_last == 1 {
break;
}
counter += 1;
}
Ok(())
}
fn nonce(prefix: &[u8; 8], counter: u32) -> Nonce<aes_gcm::aead::consts::U12> {
let mut n = [0u8; 12];
n[..8].copy_from_slice(prefix);
n[8..].copy_from_slice(&counter.to_be_bytes());
*Nonce::from_slice(&n)
}
fn aad(counter: u32, is_last: bool) -> Vec<u8> {
let mut a = counter.to_be_bytes().to_vec();
a.push(is_last as u8);
a
}
fn read_full<R: Read>(reader: &mut R, buf: &mut [u8]) -> std::io::Result<usize> {
let mut n = 0;
while n < buf.len() {
match reader.read(&mut buf[n..])? {
0 => break,
k => n += k,
}
}
Ok(n)
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(data: &[u8], chunk: usize) -> Vec<u8> {
let key = gen_key();
let mut ct = Vec::new();
encrypt(&key, chunk, data, &mut ct).unwrap();
let mut pt = Vec::new();
decrypt(&key, &ct[..], &mut pt).unwrap();
pt
}
#[test]
fn roundtrips_across_sizes_and_chunk_boundaries() {
assert_eq!(roundtrip(b"", 16), b"");
assert_eq!(roundtrip(b"hi", 16), b"hi");
assert_eq!(roundtrip(&[7u8; 16], 16), vec![7u8; 16]);
assert_eq!(roundtrip(&[8u8; 32], 16), vec![8u8; 32]);
assert_eq!(roundtrip(&[9u8; 1000], 64), vec![9u8; 1000]);
}
#[test]
fn wrong_key_fails() {
let mut ct = Vec::new();
encrypt(&gen_key(), 16, &b"secret"[..], &mut ct).unwrap();
let mut pt = Vec::new();
assert!(decrypt(&gen_key(), &ct[..], &mut pt).is_err());
}
#[test]
fn tampering_is_detected() {
let key = gen_key();
let mut ct = Vec::new();
encrypt(&key, 16, &[1u8; 50][..], &mut ct).unwrap();
let last = ct.len() - 1;
ct[last] ^= 1; let mut pt = Vec::new();
assert!(decrypt(&key, &ct[..], &mut pt).is_err());
}
#[test]
fn truncation_is_detected() {
let key = gen_key();
let mut ct = Vec::new();
encrypt(&key, 16, &[1u8; 50][..], &mut ct).unwrap(); ct.truncate(ct.len() / 2); let mut pt = Vec::new();
assert!(decrypt(&key, &ct[..], &mut pt).is_err());
}
#[test]
fn key_fragment_round_trips() {
let key = gen_key();
assert_eq!(key_from_fragment(&key_to_fragment(&key)).unwrap(), key);
}
#[test]
fn pbkdf2_matches_standard_vector() {
let mut key = [0u8; 32];
pbkdf2::pbkdf2_hmac::<sha2::Sha256>(b"password", b"salt", 1, &mut key);
let expect = [
0x12, 0x0f, 0xb6, 0xcf, 0xfc, 0xf8, 0xb3, 0x2c, 0x43, 0xe7, 0x22, 0x52, 0x56, 0xc4,
0xf8, 0x37, 0xa8, 0x65, 0x48, 0xc9, 0x2c, 0xcc, 0x35, 0x48, 0x08, 0x05, 0x98, 0x7c,
0xb7, 0x0b, 0xe1, 0x7b,
];
assert_eq!(key, expect);
}
#[test]
fn derive_key_binds_pin_and_fragment() {
let frag = [7u8; 32];
assert_eq!(derive_key("4917", &frag), derive_key("4917", &frag));
assert_ne!(derive_key("4917", &frag), derive_key("0000", &frag));
assert_ne!(derive_key("4917", &frag), derive_key("4917", &[8u8; 32]));
}
#[test]
fn meta_round_trips_and_rejects_wrong_secret() {
let secret = [3u8; 32];
let plaintext = br#"{"name":"q3 report.pdf","from":"Alex","msg":"the codes"}"#;
let blob = encrypt_meta(&secret, plaintext);
assert_eq!(decrypt_meta(&secret, &blob).unwrap(), plaintext);
assert!(decrypt_meta(&[4u8; 32], &blob).is_err()); let fixture = serde_json::json!({
"secret_hex": hex_lower(&secret),
"blob_b64url": blob,
"plaintext": String::from_utf8_lossy(plaintext),
});
let _ = std::fs::write("/tmp/dove-meta-fixture.json", fixture.to_string());
}
#[test]
fn share_id_is_maced_and_deterministic() {
let secret = crypto_secret();
let nonce = [0x11u8; 8];
let id = mint_share_id(&secret, &nonce);
assert_eq!(id.len(), 32); assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(mint_share_id(&secret, &nonce), id);
assert_eq!(&id[..16], "1111111111111111");
let other = mint_share_id(&[9u8; 32], &nonce);
assert_eq!(&other[..16], "1111111111111111");
assert_ne!(&other[16..], &id[16..]);
let fixture = serde_json::json!({
"secret_hex": hex_lower(&secret), "nonce_hex": hex_lower(&nonce), "id": id,
});
let _ = std::fs::write("/tmp/dove-macid-fixture.json", fixture.to_string());
}
fn crypto_secret() -> [u8; 32] {
let mut s = [0u8; 32];
for (i, b) in s.iter_mut().enumerate() {
*b = i as u8;
}
s
}
#[test]
fn hex_decode_round_trips() {
assert_eq!(hex_decode("00ff10").unwrap(), vec![0x00, 0xff, 0x10]);
assert_eq!(hex_decode("abc"), None); assert_eq!(hex_decode("zz"), None); }
#[test]
fn pin_hash_is_stable_and_salted_by_id() {
assert_eq!(pin_hash("abc123", "4917"), pin_hash("abc123", "4917"));
assert_ne!(pin_hash("abc123", "4917"), pin_hash("def456", "4917"));
assert_eq!(pin_hash("abc123", "4917").len(), 64);
}
}