mod inner;
#[cfg(all(test, feature = "hkdf"))]
mod cctv;
pub use inner::StreamWriter;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::sync::{Arc, Mutex, MutexGuard};
use zeroize::{Zeroize, Zeroizing};
pub const PAYLOAD_KEY_SIZE: usize = 32;
pub struct PayloadKey([u8; PAYLOAD_KEY_SIZE]);
impl PayloadKey {
pub fn from_bytes(bin: &[u8; PAYLOAD_KEY_SIZE]) -> Self {
Self(*bin)
}
pub fn to_bytes(&self) -> Zeroizing<[u8; PAYLOAD_KEY_SIZE]> {
Zeroizing::new(self.0)
}
}
impl From<[u8; PAYLOAD_KEY_SIZE]> for PayloadKey {
fn from(bin: [u8; PAYLOAD_KEY_SIZE]) -> Self {
Self(bin)
}
}
impl Drop for PayloadKey {
fn drop(&mut self) {
self.0.zeroize();
}
}
struct Source<R> {
reader: R,
consumed: u64,
start: Option<u64>,
}
impl<R: Seek> Source<R> {
fn start(&mut self) -> io::Result<u64> {
if let Some(start) = self.start {
return Ok(start);
}
let position = self.reader.stream_position()?;
let start = position.checked_sub(self.consumed).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"STREAM source position is behind the bytes read from it",
)
})?;
self.start = Some(start);
Ok(start)
}
}
struct SharedSource<R>(Arc<Mutex<Source<R>>>);
impl<R> SharedSource<R> {
fn lock(&self) -> io::Result<MutexGuard<'_, Source<R>>> {
self.0
.lock()
.map_err(|_| io::Error::other("STREAM source lock poisoned"))
}
}
impl<R> Clone for SharedSource<R> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<R: Read> Read for SharedSource<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut source = self.lock()?;
let n = source.reader.read(buf)?;
if source.start.is_none() {
source.consumed += n as u64;
}
Ok(n)
}
}
impl<R: Seek> Seek for SharedSource<R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.lock()?.reader.seek(pos)
}
}
pub struct StreamReader<R> {
decoder: inner::StreamReader<SharedSource<R>>,
source: SharedSource<R>,
key: PayloadKey,
position: u64,
stale: bool,
seek_failed: bool,
}
impl<R: Read> StreamReader<R> {
fn new(key: PayloadKey, reader: R) -> Self {
let source = SharedSource(Arc::new(Mutex::new(Source {
reader,
consumed: 0,
start: None,
})));
Self {
decoder: Self::decoder(&key, &source),
source,
key,
position: 0,
stale: false,
seek_failed: false,
}
}
fn decoder(key: &PayloadKey, source: &SharedSource<R>) -> inner::StreamReader<SharedSource<R>> {
inner::Stream::decrypt(inner::PayloadKey(key.0.into()), source.clone())
}
}
impl<R: Read + Seek> StreamReader<R> {
fn reset(&mut self) -> io::Result<()> {
{
let mut source = self.source.lock()?;
let start = source.start()?;
source.reader.seek(SeekFrom::Start(start))?;
}
self.decoder = Self::decoder(&self.key, &self.source);
self.stale = false;
Ok(())
}
}
impl<R: Read> Read for StreamReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.seek_failed {
return Err(io::Error::other("STREAM read after a failed seek"));
}
match self.decoder.read(buf) {
Ok(n) => {
self.position += n as u64;
self.stale = false;
Ok(n)
}
Err(err) => {
self.stale = true;
Err(err)
}
}
}
}
impl<R: Read + Seek> Seek for StreamReader<R> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let pos = match pos {
SeekFrom::Current(offset) => {
let target = self.position.checked_add_signed(offset).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a negative or overflowing position",
)
})?;
SeekFrom::Start(target)
}
pos => pos,
};
self.source.lock()?.start()?;
let result = if self.stale {
self.reset().and_then(|()| self.decoder.seek(pos))
} else {
self.decoder.seek(pos)
};
match result {
Ok(position) => {
self.position = position;
self.seek_failed = false;
Ok(position)
}
Err(err) => {
self.stale = true;
self.seek_failed = true;
Err(err)
}
}
}
}
pub struct Stream;
impl Stream {
pub fn encrypt<W: Write>(key: PayloadKey, writer: W) -> StreamWriter<W> {
inner::Stream::encrypt(inner::PayloadKey(key.0.into()), writer)
}
pub fn decrypt<R: Read>(key: PayloadKey, reader: R) -> StreamReader<R> {
StreamReader::new(key, reader)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const PLAINTEXT_SIZE: u64 = 64 * 1024 + 1;
const PREFIX_SIZE: u64 = 23;
const FLAKE_OFFSET: u64 = 100;
fn byte_at(position: u64) -> u8 {
(position % 251) as u8
}
fn ciphertext() -> Vec<u8> {
let plaintext: Vec<u8> = (0..PLAINTEXT_SIZE).map(byte_at).collect();
let mut writer = Stream::encrypt(PayloadKey::from([7; 32]), vec![0; PREFIX_SIZE as usize]);
writer.write_all(&plaintext).unwrap();
writer.finish().unwrap()
}
fn open<R: Read + Seek>(mut source: R) -> StreamReader<R> {
source.seek(SeekFrom::Start(PREFIX_SIZE)).unwrap();
Stream::decrypt(PayloadKey::from([7; 32]), source)
}
fn open_flaky() -> StreamReader<FlakySource> {
open(FlakySource {
inner: Cursor::new(ciphertext()),
flaked: false,
})
}
fn read_byte<R: Read>(reader: &mut R) -> io::Result<u8> {
let mut byte = [0];
reader.read_exact(&mut byte)?;
Ok(byte[0])
}
struct FlakySource {
inner: Cursor<Vec<u8>>,
flaked: bool,
}
impl Read for FlakySource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let flake = PREFIX_SIZE + FLAKE_OFFSET;
let position = self.inner.position();
if position == flake && !self.flaked {
self.flaked = true;
return Err(io::Error::other("transient failure"));
}
let len = if position < flake {
buf.len().min((flake - position) as usize)
} else {
buf.len()
};
self.inner.read(&mut buf[..len])
}
}
impl Seek for FlakySource {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
}
#[test]
fn test_seek_after_authentication_failure() {
let mut damaged = ciphertext();
damaged[PREFIX_SIZE as usize] ^= 1;
let last = PLAINTEXT_SIZE - 1;
for cached in [false, true] {
for failures in [1, 2] {
let case = format!("cached={cached} failures={failures}");
let mut reader = open(Cursor::new(damaged.clone()));
if cached {
assert_eq!(
reader.seek(SeekFrom::End(0)).unwrap(),
PLAINTEXT_SIZE,
"{case}"
);
reader.rewind().unwrap();
}
for _ in 0..failures {
assert!(read_byte(&mut reader).is_err(), "{case}");
}
assert_eq!(reader.seek(SeekFrom::Start(last)).unwrap(), last, "{case}");
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(last), "{case}");
assert_eq!(reader.read(&mut [0]).unwrap(), 0, "{case}");
reader.rewind().unwrap();
assert!(read_byte(&mut reader).is_err(), "{case}");
}
}
}
#[test]
fn test_seek_after_io_failure() {
let last = PLAINTEXT_SIZE - 1;
let targets = [
(SeekFrom::Start(17), 17),
(SeekFrom::Start(last), last),
(SeekFrom::Current(17), 17),
(SeekFrom::End(-1), last),
];
for cached in [false, true] {
for (seek, position) in targets {
let case = format!("cached={cached} seek={seek:?}");
let mut reader = open_flaky();
if cached {
assert_eq!(
reader.seek(SeekFrom::End(0)).unwrap(),
PLAINTEXT_SIZE,
"{case}"
);
reader.rewind().unwrap();
}
assert!(read_byte(&mut reader).is_err(), "{case}");
assert_eq!(reader.seek(seek).unwrap(), position, "{case}");
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(position), "{case}");
}
}
}
#[test]
fn test_retry_after_io_failure() {
let mut reader = open_flaky();
assert!(read_byte(&mut reader).is_err());
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(0));
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(1));
let last = PLAINTEXT_SIZE - 1;
assert_eq!(reader.seek(SeekFrom::Start(last)).unwrap(), last);
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(last));
}
#[test]
fn test_read_after_failed_seek() {
let mut truncated = ciphertext();
truncated.truncate(truncated.len() - (1 + 16));
let mut reader = open(Cursor::new(truncated));
assert!(reader.seek(SeekFrom::End(0)).is_err());
assert!(reader.read(&mut [0]).is_err());
reader.rewind().unwrap();
let mut plaintext = Vec::new();
assert!(reader.read_to_end(&mut plaintext).is_err());
}
#[test]
fn test_invalid_relative_seek() {
let mut reader = open(Cursor::new(ciphertext()));
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(0));
let err = reader.seek(SeekFrom::Current(-2)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert_eq!(read_byte(&mut reader).unwrap(), byte_at(1));
}
}