use std::sync::atomic::{AtomicU64, Ordering};
use ring::aead::{self, AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use tracing::{trace, warn};
use crate::error::IpcError;
use crate::ipc::IpcConnection;
const NONCE_WARN_THRESHOLD: u64 = 1 << 31; const NONCE_HARD_LIMIT: u64 = 1 << 32;
pub struct EncryptedIpcConnection {
inner: IpcConnection,
seal_key: LessSafeKey,
open_key: LessSafeKey,
send_counter: AtomicU64,
}
impl EncryptedIpcConnection {
pub fn new(inner: IpcConnection, key: &[u8; 32]) -> Result<Self, IpcError> {
let seal_unbound = UnboundKey::new(&AES_256_GCM, key).map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid AES-256-GCM key",
))
})?;
let open_unbound = UnboundKey::new(&AES_256_GCM, key).map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid AES-256-GCM key",
))
})?;
Ok(Self {
inner,
seal_key: LessSafeKey::new(seal_unbound),
open_key: LessSafeKey::new(open_unbound),
send_counter: AtomicU64::new(0),
})
}
pub async fn bind_and_accept(
path: &std::path::Path,
key: &[u8; 32],
) -> Result<(crate::ipc::IpcServer, Self), IpcError> {
let server = crate::ipc::IpcServer::bind(path)?;
let conn = server.accept().await?;
let encrypted = Self::new(conn, key)?;
Ok((server, encrypted))
}
pub async fn connect(path: &std::path::Path, key: &[u8; 32]) -> Result<Self, IpcError> {
let conn = IpcConnection::connect(path).await?;
Self::new(conn, key)
}
fn make_nonce(&self) -> Result<[u8; 12], IpcError> {
let counter = self.send_counter.fetch_add(1, Ordering::Relaxed);
if counter >= NONCE_HARD_LIMIT {
return Err(IpcError::Io(std::io::Error::other(
"AES-GCM nonce limit reached (2^32 messages) — rotate key with rekey()",
)));
}
if counter == NONCE_WARN_THRESHOLD {
warn!(
counter,
"ipc-encrypted: approaching nonce limit (2^31 of 2^32) — rotate key soon"
);
}
let mut nonce_bytes = [0u8; 12];
nonce_bytes[4..12].copy_from_slice(&counter.to_be_bytes());
Ok(nonce_bytes)
}
pub async fn send(&mut self, payload: &serde_json::Value) -> Result<(), IpcError> {
let plaintext = serde_json::to_vec(payload)?;
let nonce_bytes = self.make_nonce()?;
let nonce = Nonce::assume_unique_for_key(nonce_bytes);
let mut in_out = plaintext;
self.seal_key
.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
.map_err(|_| IpcError::Io(std::io::Error::other("encryption failed")))?;
let mut frame = Vec::with_capacity(12 + in_out.len());
frame.extend_from_slice(&nonce_bytes);
frame.extend_from_slice(&in_out);
use serde_json::Value;
let encoded = Value::String(base64_encode(&frame));
self.inner.send(&encoded).await?;
trace!(size = frame.len(), "ipc-encrypted: sent");
Ok(())
}
pub async fn recv(&mut self) -> Result<serde_json::Value, IpcError> {
let raw = self.inner.recv().await?;
let encoded_str = raw.as_str().ok_or_else(|| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"expected base64 string in encrypted frame",
))
})?;
let frame = base64_decode(encoded_str).map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid base64 in encrypted frame",
))
})?;
if frame.len() < 12 + aead::AES_256_GCM.tag_len() {
return Err(IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"encrypted frame too short",
)));
}
let (nonce_bytes, ciphertext_and_tag) = frame.split_at(12);
let nonce_arr: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
IpcError::Io(std::io::Error::other(
"internal error: nonce must be 12 bytes",
))
})?;
let nonce = Nonce::assume_unique_for_key(nonce_arr);
let mut in_out = ciphertext_and_tag.to_vec();
let plaintext = self
.open_key
.open_in_place(nonce, Aad::empty(), &mut in_out)
.map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"decryption failed (bad key or tampered data)",
))
})?;
let value: serde_json::Value = serde_json::from_slice(plaintext)?;
trace!(size = plaintext.len(), "ipc-encrypted: received");
Ok(value)
}
pub fn rekey(&mut self, new_key: &[u8; 32]) -> Result<(), IpcError> {
let seal = UnboundKey::new(&AES_256_GCM, new_key).map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid AES-256-GCM key",
))
})?;
let open = UnboundKey::new(&AES_256_GCM, new_key).map_err(|_| {
IpcError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid AES-256-GCM key",
))
})?;
self.seal_key = LessSafeKey::new(seal);
self.open_key = LessSafeKey::new(open);
self.send_counter.store(0, Ordering::Relaxed);
trace!("ipc-encrypted: rekeyed, nonce counter reset");
Ok(())
}
#[inline]
pub fn messages_sent(&self) -> u64 {
self.send_counter.load(Ordering::Relaxed)
}
#[inline]
#[must_use]
pub fn needs_rekey(&self) -> bool {
self.send_counter.load(Ordering::Relaxed) >= NONCE_WARN_THRESHOLD
}
pub fn inner(&self) -> &IpcConnection {
&self.inner
}
pub fn into_inner(self) -> IpcConnection {
self.inner
}
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
result.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(ALPHABET[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}
fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
fn decode_char(c: u8) -> Result<u32, ()> {
match c {
b'A'..=b'Z' => Ok((c - b'A') as u32),
b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
b'+' => Ok(62),
b'/' => Ok(63),
b'=' => Ok(0),
_ => Err(()),
}
}
let bytes = input.as_bytes();
if !bytes.len().is_multiple_of(4) {
return Err(());
}
let mut result = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
let a = decode_char(chunk[0])?;
let b = decode_char(chunk[1])?;
let c = decode_char(chunk[2])?;
let d = decode_char(chunk[3])?;
let triple = (a << 18) | (b << 12) | (c << 6) | d;
result.push(((triple >> 16) & 0xFF) as u8);
if chunk[2] != b'=' {
result.push(((triple >> 8) & 0xFF) as u8);
}
if chunk[3] != b'=' {
result.push((triple & 0xFF) as u8);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_roundtrip() {
let data = b"hello, majra encrypted IPC!";
let encoded = base64_encode(data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn base64_empty() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
}
#[test]
fn base64_padding() {
let enc = base64_encode(b"a");
assert!(enc.ends_with("=="));
assert_eq!(base64_decode(&enc).unwrap(), b"a");
let enc = base64_encode(b"ab");
assert!(enc.ends_with('='));
assert_eq!(base64_decode(&enc).unwrap(), b"ab");
let enc = base64_encode(b"abc");
assert!(!enc.contains('='));
assert_eq!(base64_decode(&enc).unwrap(), b"abc");
}
#[test]
fn nonce_monotonic() {
let conn_key = [0u8; 32];
let counter = AtomicU64::new(0);
let n1 = counter.fetch_add(1, Ordering::Relaxed);
let n2 = counter.fetch_add(1, Ordering::Relaxed);
assert_eq!(n1, 0);
assert_eq!(n2, 1);
let _ = UnboundKey::new(&AES_256_GCM, &conn_key).unwrap();
}
#[test]
fn encrypt_decrypt_roundtrip() {
let key = [42u8; 32];
let seal_key = LessSafeKey::new(UnboundKey::new(&AES_256_GCM, &key).unwrap());
let open_key = LessSafeKey::new(UnboundKey::new(&AES_256_GCM, &key).unwrap());
let plaintext = b"hello encrypted world";
let nonce_bytes = [0u8; 12];
let nonce = Nonce::assume_unique_for_key(nonce_bytes);
let mut in_out = plaintext.to_vec();
seal_key
.seal_in_place_append_tag(nonce, Aad::empty(), &mut in_out)
.unwrap();
let nonce2 = Nonce::assume_unique_for_key(nonce_bytes);
let decrypted = open_key
.open_in_place(nonce2, Aad::empty(), &mut in_out)
.unwrap();
assert_eq!(decrypted, plaintext);
}
}