use std::io::Write as _;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use zeroize::Zeroizing;
use crate::decrypt;
use crate::encrypt;
use crate::error::PqfileError;
use crate::writer::PqfWriter;
#[must_use = "encryption result must be checked"]
pub async fn encrypt_stream_async<R, W>(
pubkey_pem: &str,
original_size: u64,
chunk_size: usize,
reader: &mut R,
writer: &mut W,
) -> Result<(), PqfileError>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
if chunk_size == 0 {
return Err(PqfileError::EncryptionFailure);
}
if original_size > crate::format::MAX_ORIGINAL_SIZE {
return Err(PqfileError::EncryptionFailure);
}
let mut plaintext = Vec::with_capacity(original_size.min(64 * 1024 * 1024) as usize);
reader
.read_to_end(&mut plaintext)
.await
.map_err(PqfileError::Io)?;
if plaintext.len() as u64 > crate::format::MAX_ORIGINAL_SIZE {
return Err(PqfileError::EncryptionFailure);
}
let mut ct = Vec::new();
encrypt::encrypt_stream(
pubkey_pem,
plaintext.len() as u64,
chunk_size,
&mut plaintext.as_slice(),
&mut ct,
)?;
writer.write_all(&ct).await.map_err(PqfileError::Io)?;
writer.flush().await.map_err(PqfileError::Io)?;
Ok(())
}
#[must_use = "decryption result must be checked"]
pub async fn decrypt_stream_async<R, W>(
privkey_pem: &str,
reader: R,
writer: &mut W,
passphrase: Option<&str>,
) -> Result<(), PqfileError>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
let mut ct_reader = tokio::io::BufReader::new(reader);
let mut ct = Vec::new();
ct_reader
.read_to_end(&mut ct)
.await
.map_err(PqfileError::Io)?;
if ct.len() as u64 > crate::format::MAX_ORIGINAL_SIZE + (1 << 20) {
return Err(PqfileError::DecryptionFailure);
}
let mut plaintext = Zeroizing::new(Vec::new());
decrypt::decrypt_stream(privkey_pem, &mut ct.as_slice(), &mut *plaintext, passphrase)?;
writer
.write_all(&plaintext)
.await
.map_err(PqfileError::Io)?;
writer.flush().await.map_err(PqfileError::Io)?;
Ok(())
}
enum AsyncWriterState<W: AsyncWrite + Unpin> {
Buffering {
sink: W,
plaintext: Zeroizing<Vec<u8>>,
},
Flushing { sink: W, ct: Vec<u8>, pos: usize },
Done,
}
pub struct AsyncPqfWriter<W: AsyncWrite + Unpin> {
pubkey_pem: String,
original_size: u64,
chunk_size: usize,
state: AsyncWriterState<W>,
}
impl<W: AsyncWrite + Unpin> AsyncPqfWriter<W> {
pub fn new(
sink: W,
pubkey_pem: &str,
original_size: u64,
chunk_size: usize,
) -> Result<Self, PqfileError> {
if chunk_size == 0 {
return Err(PqfileError::EncryptionFailure);
}
Ok(Self {
pubkey_pem: pubkey_pem.to_owned(),
original_size,
chunk_size,
state: AsyncWriterState::Buffering {
sink,
plaintext: Zeroizing::new(Vec::new()),
},
})
}
pub async fn finish(mut self) -> Result<W, PqfileError> {
self.seal_and_write().await
}
async fn seal_and_write(&mut self) -> Result<W, PqfileError> {
let (mut sink, plaintext) = match std::mem::replace(&mut self.state, AsyncWriterState::Done)
{
AsyncWriterState::Buffering { sink, plaintext } => (sink, plaintext),
AsyncWriterState::Flushing { mut sink, ct, pos } => {
sink.write_all(&ct[pos..]).await.map_err(PqfileError::Io)?;
sink.flush().await.map_err(PqfileError::Io)?;
return Ok(sink);
}
AsyncWriterState::Done => {
return Err(PqfileError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"AsyncPqfWriter already finished",
)));
}
};
let ct = {
let mut ct = Vec::new();
let mut w = PqfWriter::new(
&mut ct,
&self.pubkey_pem,
self.original_size,
self.chunk_size,
)?;
w.write_all(&plaintext).map_err(PqfileError::Io)?;
w.finish()?;
ct
};
sink.write_all(&ct).await.map_err(PqfileError::Io)?;
sink.flush().await.map_err(PqfileError::Io)?;
Ok(sink)
}
}
impl<W: AsyncWrite + Unpin> AsyncWrite for AsyncPqfWriter<W> {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
match &mut self.state {
AsyncWriterState::Buffering { plaintext, .. } => {
plaintext.extend_from_slice(buf);
Poll::Ready(Ok(buf.len()))
}
_ => Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"AsyncPqfWriter already finished",
))),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
match &mut self.state {
AsyncWriterState::Buffering { sink, .. } => Pin::new(sink).poll_flush(cx),
AsyncWriterState::Flushing { sink, .. } => Pin::new(sink).poll_flush(cx),
AsyncWriterState::Done => Poll::Ready(Ok(())),
}
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
if let AsyncWriterState::Buffering { .. } = &self.state {
let (sink, plaintext) = match std::mem::replace(&mut self.state, AsyncWriterState::Done)
{
AsyncWriterState::Buffering { sink, plaintext } => (sink, plaintext),
_ => unreachable!(),
};
let ct = match (|| -> Result<Vec<u8>, PqfileError> {
let mut ct = Vec::new();
let mut w = PqfWriter::new(
&mut ct,
&self.pubkey_pem,
self.original_size,
self.chunk_size,
)?;
w.write_all(&plaintext).map_err(PqfileError::Io)?;
w.finish()?;
Ok(ct)
})() {
Ok(ct) => ct,
Err(e) => {
return Poll::Ready(Err(std::io::Error::other(e.to_string())));
}
};
self.state = AsyncWriterState::Flushing { sink, ct, pos: 0 };
}
loop {
match &mut self.state {
AsyncWriterState::Flushing { sink, ct, pos } => {
if *pos < ct.len() {
match Pin::new(&mut *sink).poll_write(cx, &ct[*pos..]) {
Poll::Ready(Ok(n)) => {
*pos += n;
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
} else {
match Pin::new(&mut *sink).poll_shutdown(cx) {
Poll::Ready(r) => {
self.state = AsyncWriterState::Done;
return Poll::Ready(r);
}
Poll::Pending => return Poll::Pending,
}
}
}
AsyncWriterState::Done => return Poll::Ready(Ok(())),
AsyncWriterState::Buffering { .. } => unreachable!(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::CHUNK_SIZE;
use crate::keygen::keygen_bytes;
#[tokio::test]
async fn async_roundtrip_small() {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext = b"hello async pqfile world";
let mut ct = Vec::new();
encrypt_stream_async(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
)
.await
.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert_eq!(out, plaintext);
}
#[tokio::test]
async fn async_roundtrip_multi_chunk() {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext: Vec<u8> = (0u8..=255).cycle().take(CHUNK_SIZE * 3 + 7).collect();
let mut ct = Vec::new();
encrypt_stream_async(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
)
.await
.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert_eq!(out, plaintext);
}
#[tokio::test]
async fn async_roundtrip_empty() {
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let mut ct = Vec::new();
encrypt_stream_async(&pub_pem, 0, CHUNK_SIZE, &mut [].as_slice(), &mut ct)
.await
.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert!(out.is_empty());
}
#[tokio::test]
async fn async_roundtrip_with_passphrase() {
let (pub_pem, priv_pem) = keygen_bytes(768, Some("async-pass")).unwrap();
let plaintext = b"passphrase-protected async roundtrip";
let mut ct = Vec::new();
encrypt_stream_async(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
)
.await
.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, Some("async-pass"))
.await
.unwrap();
assert_eq!(out, plaintext.as_slice());
}
#[tokio::test]
async fn async_ciphertext_matches_sync() {
use crate::decrypt::decrypt_stream as sync_decrypt;
use crate::encrypt::encrypt_stream as sync_encrypt;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext = b"async / sync interop check";
let mut ct = Vec::new();
encrypt_stream_async(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct,
)
.await
.unwrap();
let mut out = Vec::new();
sync_decrypt(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert_eq!(out, plaintext);
let mut ct2 = Vec::new();
sync_encrypt(
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
&mut plaintext.as_slice(),
&mut ct2,
)
.unwrap();
let mut out2 = Vec::new();
decrypt_stream_async(&priv_pem, ct2.as_slice(), &mut out2, None)
.await
.unwrap();
assert_eq!(out2, plaintext);
}
#[tokio::test]
async fn async_rejects_zero_chunk_size() {
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
let result =
encrypt_stream_async(&pub_pem, 0, 0, &mut [].as_slice(), &mut Vec::new()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn async_writer_roundtrip_empty() {
use super::AsyncPqfWriter;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let w = AsyncPqfWriter::new(Vec::<u8>::new(), &pub_pem, 0, CHUNK_SIZE).unwrap();
let ct = w.finish().await.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert!(out.is_empty());
}
#[tokio::test]
async fn async_writer_roundtrip_small() {
use super::AsyncPqfWriter;
use tokio::io::AsyncWriteExt;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext = b"hello async pqfwriter";
let mut w = AsyncPqfWriter::new(
Vec::<u8>::new(),
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
)
.unwrap();
w.write_all(plaintext).await.unwrap();
let ct = w.finish().await.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert_eq!(out, plaintext);
}
#[tokio::test]
async fn async_writer_roundtrip_multi_chunk() {
use super::AsyncPqfWriter;
use tokio::io::AsyncWriteExt;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext: Vec<u8> = (0u8..=255).cycle().take(CHUNK_SIZE * 2 + 77).collect();
let mut w = AsyncPqfWriter::new(
Vec::<u8>::new(),
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
)
.unwrap();
w.write_all(&plaintext).await.unwrap();
let ct = w.finish().await.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert_eq!(out, plaintext);
}
#[tokio::test]
async fn async_writer_shutdown_seals() {
use super::AsyncPqfWriter;
use tokio::io::AsyncWriteExt;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext = b"shutdown seals the stream";
let mut ct = Vec::new();
let mut w =
AsyncPqfWriter::new(&mut ct, &pub_pem, plaintext.len() as u64, CHUNK_SIZE).unwrap();
w.write_all(plaintext).await.unwrap();
w.shutdown().await.unwrap();
let mut out = Vec::new();
decrypt_stream_async(&priv_pem, ct.as_slice(), &mut out, None)
.await
.unwrap();
assert_eq!(out, plaintext.as_slice());
}
#[tokio::test]
async fn async_writer_interop_with_sync_reader() {
use super::AsyncPqfWriter;
use crate::decrypt::decrypt_stream as sync_decrypt;
use tokio::io::AsyncWriteExt;
let (pub_pem, priv_pem) = keygen_bytes(768, None).unwrap();
let plaintext = b"async writer -> sync reader";
let mut w = AsyncPqfWriter::new(
Vec::<u8>::new(),
&pub_pem,
plaintext.len() as u64,
CHUNK_SIZE,
)
.unwrap();
w.write_all(plaintext).await.unwrap();
let ct = w.finish().await.unwrap();
let mut out = Vec::new();
sync_decrypt(&priv_pem, &mut ct.as_slice(), &mut out, None).unwrap();
assert_eq!(out, plaintext.as_slice());
}
#[tokio::test]
async fn async_writer_rejects_zero_chunk_size() {
use super::AsyncPqfWriter;
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
let result = AsyncPqfWriter::new(Vec::<u8>::new(), &pub_pem, 0, 0);
assert!(result.is_err());
}
#[tokio::test]
async fn async_encrypt_rejects_original_size_above_max() {
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
let oversized = crate::format::MAX_ORIGINAL_SIZE + 1;
let result = encrypt_stream_async(
&pub_pem,
oversized,
CHUNK_SIZE,
&mut [].as_slice(),
&mut Vec::new(),
)
.await;
assert!(
result.is_err(),
"expected error for oversized original_size"
);
}
#[tokio::test]
async fn async_decrypt_rejects_oversized_ciphertext() {
let (_, priv_pem) = keygen_bytes(768, None).unwrap();
let garbage: Vec<u8> = vec![0xFFu8; 64];
let mut out = Vec::new();
let result = decrypt_stream_async(&priv_pem, garbage.as_slice(), &mut out, None).await;
assert!(result.is_err(), "garbage ciphertext must be rejected");
}
}