use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use zeroize::Zeroizing;
use crate::replay_window::ReplayWindow;
use crate::WireError;
#[cfg(feature = "consent")]
use crate::consent::ConsentEvent;
pub const DEFAULT_REKEY_GRACE: Duration = Duration::from_secs(5);
pub struct Session {
session_key: Option<Zeroizing<[u8; 32]>>,
prev_session_key: Option<Zeroizing<[u8; 32]>>,
key_established_at: Option<Instant>,
prev_key_expires_at: Option<Instant>,
nonce_counter: u64,
source_id: [u8; 8],
epoch: u8,
replay_window: ReplayWindow,
rekey_grace: Duration,
current_key_epoch: u8,
prev_key_epoch: Option<u8>,
#[cfg(feature = "consent")]
consent_state: crate::consent::ConsentState,
#[cfg(feature = "consent")]
active_request_id: Option<u64>,
#[cfg(feature = "consent")]
last_response_approved: Option<bool>,
}
impl Session {
pub fn new() -> Self {
Self::with_source_id(rand::random(), rand::random())
}
pub fn with_source_id(source_id: [u8; 8], epoch: u8) -> Self {
Self {
session_key: None,
prev_session_key: None,
key_established_at: None,
prev_key_expires_at: None,
nonce_counter: 0,
source_id,
epoch,
replay_window: ReplayWindow::new(),
rekey_grace: DEFAULT_REKEY_GRACE,
current_key_epoch: 0,
prev_key_epoch: None,
#[cfg(feature = "consent")]
consent_state: crate::consent::ConsentState::LegacyBypass,
#[cfg(feature = "consent")]
active_request_id: None,
#[cfg(feature = "consent")]
last_response_approved: None,
}
}
pub fn with_rekey_grace(mut self, grace: Duration) -> Self {
self.rekey_grace = grace;
self
}
pub fn install_key(&mut self, key: [u8; 32]) {
if self.session_key.is_some() {
self.prev_session_key = self.session_key.take();
self.prev_key_expires_at = Some(Instant::now() + self.rekey_grace);
self.prev_key_epoch = Some(self.current_key_epoch);
self.current_key_epoch = self.current_key_epoch.wrapping_add(1);
}
self.session_key = Some(Zeroizing::new(key));
self.key_established_at = Some(Instant::now());
self.nonce_counter = 0;
}
pub fn has_key(&self) -> bool {
self.session_key.is_some()
}
pub fn tick(&mut self) {
if let Some(expires) = self.prev_key_expires_at {
if Instant::now() > expires {
self.prev_session_key = None;
self.prev_key_expires_at = None;
if let Some(old_epoch) = self.prev_key_epoch.take() {
self.replay_window.drop_epoch(old_epoch);
}
}
}
}
pub fn next_nonce(&mut self) -> u64 {
let n = self.nonce_counter;
self.nonce_counter = self.nonce_counter.wrapping_add(1);
n
}
pub fn nonce_counter(&self) -> u64 {
self.nonce_counter
}
pub fn source_id(&self) -> &[u8; 8] {
&self.source_id
}
pub fn epoch(&self) -> u8 {
self.epoch
}
pub fn key_established_at(&self) -> Option<Instant> {
self.key_established_at
}
#[cfg(feature = "consent")]
pub fn consent_state(&self) -> crate::consent::ConsentState {
self.consent_state
}
#[cfg(feature = "consent")]
pub fn session_fingerprint(&self, request_id: u64) -> Result<[u8; 32], WireError> {
let key = self.session_key.as_ref().ok_or(WireError::NoSessionKey)?;
Ok(self.session_fingerprint_from_key(request_id, key))
}
#[cfg(feature = "consent")]
fn session_fingerprint_from_key(&self, request_id: u64, key: &[u8; 32]) -> [u8; 32] {
use hkdf::Hkdf;
use sha2::Sha256;
let mut info = [0u8; 8 + 1 + 8];
info[..8].copy_from_slice(&self.source_id);
info[8] = self.epoch;
info[9..17].copy_from_slice(&request_id.to_be_bytes());
let hk = Hkdf::<Sha256>::new(Some(b"xenia-session-fingerprint-v1"), key);
let mut out = [0u8; 32];
hk.expand(&info, &mut out)
.expect("HKDF-SHA-256 expand of 32 bytes cannot fail");
out
}
#[cfg(feature = "consent")]
fn verify_fingerprint_either_epoch(
&self,
request_id: u64,
claimed: &[u8; 32],
) -> bool {
let current_match = match self.session_key.as_ref() {
Some(key) => {
let fp = self.session_fingerprint_from_key(request_id, key);
ct_eq_32(&fp, claimed)
}
None => false,
};
let prev_match = match self.prev_session_key.as_ref() {
Some(prev) => {
let fp = self.session_fingerprint_from_key(request_id, prev);
ct_eq_32(&fp, claimed)
}
None => false,
};
current_match | prev_match
}
#[cfg(feature = "consent")]
pub fn sign_consent_request(
&self,
mut core: crate::consent::ConsentRequestCore,
signing_key: &ed25519_dalek::SigningKey,
) -> Result<crate::consent::ConsentRequest, WireError> {
core.session_fingerprint = self.session_fingerprint(core.request_id)?;
Ok(crate::consent::ConsentRequest::sign(core, signing_key))
}
#[cfg(feature = "consent")]
pub fn sign_consent_response(
&self,
mut core: crate::consent::ConsentResponseCore,
signing_key: &ed25519_dalek::SigningKey,
) -> Result<crate::consent::ConsentResponse, WireError> {
core.session_fingerprint = self.session_fingerprint(core.request_id)?;
Ok(crate::consent::ConsentResponse::sign(core, signing_key))
}
#[cfg(feature = "consent")]
pub fn sign_consent_revocation(
&self,
mut core: crate::consent::ConsentRevocationCore,
signing_key: &ed25519_dalek::SigningKey,
) -> Result<crate::consent::ConsentRevocation, WireError> {
core.session_fingerprint = self.session_fingerprint(core.request_id)?;
Ok(crate::consent::ConsentRevocation::sign(core, signing_key))
}
#[cfg(feature = "consent")]
pub fn verify_consent_request(
&self,
req: &crate::consent::ConsentRequest,
expected_pubkey: Option<&[u8; 32]>,
) -> bool {
if !req.verify(expected_pubkey) {
return false;
}
self.verify_fingerprint_either_epoch(req.core.request_id, &req.core.session_fingerprint)
}
#[cfg(feature = "consent")]
pub fn verify_consent_response(
&self,
resp: &crate::consent::ConsentResponse,
expected_pubkey: Option<&[u8; 32]>,
) -> bool {
if !resp.verify(expected_pubkey) {
return false;
}
self.verify_fingerprint_either_epoch(
resp.core.request_id,
&resp.core.session_fingerprint,
)
}
#[cfg(feature = "consent")]
pub fn verify_consent_revocation(
&self,
rev: &crate::consent::ConsentRevocation,
expected_pubkey: Option<&[u8; 32]>,
) -> bool {
if !rev.verify(expected_pubkey) {
return false;
}
self.verify_fingerprint_either_epoch(rev.core.request_id, &rev.core.session_fingerprint)
}
#[cfg(feature = "consent")]
pub fn observe_consent(
&mut self,
event: ConsentEvent,
) -> Result<crate::consent::ConsentState, crate::consent::ConsentViolation> {
use crate::consent::{ConsentState, ConsentViolation};
if self.consent_state == ConsentState::LegacyBypass {
return Ok(self.consent_state);
}
let event_id = event.request_id();
match (self.consent_state, event) {
(ConsentState::AwaitingRequest, ConsentEvent::Request { request_id }) => {
self.consent_state = ConsentState::Requested;
self.active_request_id = Some(request_id);
self.last_response_approved = None;
}
(ConsentState::AwaitingRequest, ConsentEvent::ResponseApproved { .. })
| (ConsentState::AwaitingRequest, ConsentEvent::ResponseDenied { .. }) => {
return Err(ConsentViolation::StaleResponseForUnknownRequest {
request_id: event_id,
});
}
(ConsentState::AwaitingRequest, ConsentEvent::Revocation { .. }) => {
return Err(ConsentViolation::RevocationBeforeApproval {
request_id: event_id,
});
}
(ConsentState::Requested, ConsentEvent::Request { request_id }) => {
match self.active_request_id {
Some(active) if request_id > active => {
self.active_request_id = Some(request_id);
self.last_response_approved = None;
}
_ => { }
}
}
(ConsentState::Requested, ConsentEvent::ResponseApproved { request_id }) => {
if self.active_request_id != Some(request_id) {
return Err(ConsentViolation::StaleResponseForUnknownRequest {
request_id,
});
}
self.consent_state = ConsentState::Approved;
self.last_response_approved = Some(true);
}
(ConsentState::Requested, ConsentEvent::ResponseDenied { request_id }) => {
if self.active_request_id != Some(request_id) {
return Err(ConsentViolation::StaleResponseForUnknownRequest {
request_id,
});
}
self.consent_state = ConsentState::Denied;
self.last_response_approved = Some(false);
}
(ConsentState::Requested, ConsentEvent::Revocation { .. }) => {
return Err(ConsentViolation::RevocationBeforeApproval {
request_id: event_id,
});
}
(ConsentState::Approved, ConsentEvent::Request { request_id }) => {
match self.active_request_id {
Some(active) if request_id > active => {
self.consent_state = ConsentState::Requested;
self.active_request_id = Some(request_id);
self.last_response_approved = None;
}
_ => { }
}
}
(ConsentState::Approved, ConsentEvent::ResponseApproved { request_id }) => {
match self.active_request_id {
Some(active) if active == request_id => { }
_ => {
return Err(ConsentViolation::StaleResponseForUnknownRequest {
request_id,
});
}
}
}
(ConsentState::Approved, ConsentEvent::ResponseDenied { request_id }) => {
if self.active_request_id == Some(request_id) {
return Err(ConsentViolation::ContradictoryResponse {
request_id,
prior_approved: true,
new_approved: false,
});
}
return Err(ConsentViolation::StaleResponseForUnknownRequest { request_id });
}
(ConsentState::Approved, ConsentEvent::Revocation { request_id }) => {
if self.active_request_id == Some(request_id) {
self.consent_state = ConsentState::Revoked;
}
}
(ConsentState::Denied, ConsentEvent::Request { request_id }) => {
match self.active_request_id {
Some(active) if request_id > active => {
self.consent_state = ConsentState::Requested;
self.active_request_id = Some(request_id);
self.last_response_approved = None;
}
_ => { }
}
}
(ConsentState::Denied, ConsentEvent::ResponseDenied { request_id }) => {
match self.active_request_id {
Some(active) if active == request_id => { }
_ => {
return Err(ConsentViolation::StaleResponseForUnknownRequest {
request_id,
});
}
}
}
(ConsentState::Denied, ConsentEvent::ResponseApproved { request_id }) => {
if self.active_request_id == Some(request_id) {
return Err(ConsentViolation::ContradictoryResponse {
request_id,
prior_approved: false,
new_approved: true,
});
}
return Err(ConsentViolation::StaleResponseForUnknownRequest { request_id });
}
(ConsentState::Denied, ConsentEvent::Revocation { .. }) => {
}
(ConsentState::Revoked, ConsentEvent::Request { request_id }) => {
match self.active_request_id {
Some(active) if request_id > active => {
self.consent_state = ConsentState::Requested;
self.active_request_id = Some(request_id);
self.last_response_approved = None;
}
_ => { }
}
}
(ConsentState::Revoked, _) => { }
(ConsentState::LegacyBypass, _) => unreachable!(),
}
Ok(self.consent_state)
}
#[cfg(feature = "consent")]
#[inline]
fn can_seal_frame(&self) -> Result<(), WireError> {
use crate::consent::ConsentState;
match self.consent_state {
ConsentState::LegacyBypass | ConsentState::Approved => Ok(()),
ConsentState::Revoked => Err(WireError::ConsentRevoked),
ConsentState::AwaitingRequest
| ConsentState::Requested
| ConsentState::Denied => Err(WireError::NoConsent),
}
}
pub fn seal(&mut self, plaintext: &[u8], payload_type: u8) -> Result<Vec<u8>, WireError> {
use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit, Nonce};
#[cfg(feature = "consent")]
if matches!(
payload_type,
crate::payload_types::PAYLOAD_TYPE_FRAME
| crate::payload_types::PAYLOAD_TYPE_INPUT
| crate::payload_types::PAYLOAD_TYPE_FRAME_LZ4
) {
self.can_seal_frame()?;
}
let key = self.session_key.as_ref().ok_or(WireError::NoSessionKey)?;
let key_bytes: [u8; 32] = **key;
if self.nonce_counter >= (1u64 << 32) {
return Err(WireError::SequenceExhausted);
}
let seq = (self.next_nonce() & 0xFFFF_FFFF) as u32;
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..6].copy_from_slice(&self.source_id[..6]);
nonce_bytes[6] = payload_type;
nonce_bytes[7] = self.epoch;
nonce_bytes[8..12].copy_from_slice(&seq.to_le_bytes());
let cipher = ChaCha20Poly1305::new((&key_bytes).into());
let nonce = Nonce::from(nonce_bytes);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|_| WireError::SealFailed)?;
let mut out = Vec::with_capacity(12 + ciphertext.len());
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub fn open(&mut self, envelope: &[u8]) -> Result<Vec<u8>, WireError> {
use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit, Nonce};
if envelope.len() < 12 + 16 {
return Err(WireError::OpenFailed);
}
let (nonce_bytes, ciphertext) = envelope.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let (plaintext, verified_epoch) = if let Some(key) = self.session_key.as_ref() {
let key_bytes: [u8; 32] = **key;
let cipher = ChaCha20Poly1305::new((&key_bytes).into());
if let Ok(pt) = cipher.decrypt(nonce, ciphertext) {
(Some(pt), Some(self.current_key_epoch))
} else if let (Some(prev), Some(prev_epoch)) =
(self.prev_session_key.as_ref(), self.prev_key_epoch)
{
let prev_bytes: [u8; 32] = **prev;
let cipher = ChaCha20Poly1305::new((&prev_bytes).into());
match cipher.decrypt(nonce, ciphertext) {
Ok(pt) => (Some(pt), Some(prev_epoch)),
Err(_) => (None, None),
}
} else {
(None, None)
}
} else if let (Some(prev), Some(prev_epoch)) =
(self.prev_session_key.as_ref(), self.prev_key_epoch)
{
let prev_bytes: [u8; 32] = **prev;
let cipher = ChaCha20Poly1305::new((&prev_bytes).into());
match cipher.decrypt(nonce, ciphertext) {
Ok(pt) => (Some(pt), Some(prev_epoch)),
Err(_) => (None, None),
}
} else {
return Err(WireError::NoSessionKey);
};
let plaintext = plaintext.ok_or(WireError::OpenFailed)?;
let verified_epoch =
verified_epoch.expect("AEAD succeeded so a key verified; epoch must be set");
let mut source_id_u64 = 0u64;
for (i, b) in nonce_bytes[..6].iter().enumerate() {
source_id_u64 |= (*b as u64) << (i * 8);
}
let payload_type = nonce_bytes[6];
let seq = u32::from_le_bytes([
nonce_bytes[8],
nonce_bytes[9],
nonce_bytes[10],
nonce_bytes[11],
]) as u64;
if !self
.replay_window
.accept(source_id_u64, payload_type, verified_epoch, seq)
{
return Err(WireError::OpenFailed);
}
#[cfg(feature = "consent")]
if matches!(
payload_type,
crate::payload_types::PAYLOAD_TYPE_FRAME
| crate::payload_types::PAYLOAD_TYPE_INPUT
| crate::payload_types::PAYLOAD_TYPE_FRAME_LZ4
) {
self.can_seal_frame()?;
}
Ok(plaintext)
}
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
pub struct SessionBuilder {
source_id: Option<[u8; 8]>,
epoch: Option<u8>,
rekey_grace: Duration,
#[cfg(feature = "consent")]
consent_required: bool,
replay_window_bits: u32,
}
impl SessionBuilder {
pub fn new() -> Self {
Self {
source_id: None,
epoch: None,
rekey_grace: DEFAULT_REKEY_GRACE,
#[cfg(feature = "consent")]
consent_required: false,
replay_window_bits: 64,
}
}
pub fn with_source_id(mut self, source_id: [u8; 8], epoch: u8) -> Self {
self.source_id = Some(source_id);
self.epoch = Some(epoch);
self
}
pub fn with_rekey_grace(mut self, grace: Duration) -> Self {
self.rekey_grace = grace;
self
}
#[cfg(feature = "consent")]
pub fn require_consent(mut self, require: bool) -> Self {
self.consent_required = require;
self
}
pub fn with_replay_window_bits(mut self, bits: u32) -> Self {
self.replay_window_bits = bits;
self
}
pub fn build(self) -> Session {
let source_id = self.source_id.unwrap_or_else(rand::random);
let epoch = self.epoch.unwrap_or_else(rand::random);
let replay_window = ReplayWindow::with_window_bits(self.replay_window_bits);
Session {
session_key: None,
prev_session_key: None,
key_established_at: None,
prev_key_expires_at: None,
nonce_counter: 0,
source_id,
epoch,
replay_window,
rekey_grace: self.rekey_grace,
current_key_epoch: 0,
prev_key_epoch: None,
#[cfg(feature = "consent")]
consent_state: if self.consent_required {
crate::consent::ConsentState::AwaitingRequest
} else {
crate::consent::ConsentState::LegacyBypass
},
#[cfg(feature = "consent")]
active_request_id: None,
#[cfg(feature = "consent")]
last_response_approved: None,
}
}
}
impl Default for SessionBuilder {
fn default() -> Self {
Self::new()
}
}
impl Session {
pub fn builder() -> SessionBuilder {
SessionBuilder::new()
}
}
#[cfg(feature = "consent")]
#[inline]
fn ct_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut diff: u8 = 0;
for i in 0..32 {
diff |= a[i] ^ b[i];
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_session_has_no_key() {
let s = Session::new();
assert!(!s.has_key());
}
#[test]
fn install_key_sets_has_key() {
let mut s = Session::new();
s.install_key([0x11; 32]);
assert!(s.has_key());
assert_eq!(s.nonce_counter(), 0);
}
#[test]
fn seal_fails_without_key() {
let mut s = Session::new();
assert!(matches!(s.seal(b"hi", 0x10), Err(WireError::NoSessionKey)));
}
#[test]
fn open_fails_without_key() {
let mut s = Session::new();
let envelope = [0u8; 40];
assert!(matches!(s.open(&envelope), Err(WireError::NoSessionKey)));
}
#[test]
fn open_short_envelope_fails() {
let mut s = Session::new();
s.install_key([0u8; 32]);
assert!(matches!(s.open(&[0u8; 10]), Err(WireError::OpenFailed)));
}
#[test]
fn seal_open_roundtrip() {
let mut sender = Session::with_source_id([1; 8], 0xAA);
let mut receiver = Session::with_source_id([1; 8], 0xAA);
sender.install_key([0x33; 32]);
receiver.install_key([0x33; 32]);
let sealed = sender.seal(b"hello xenia", 0x10).unwrap();
let opened = receiver.open(&sealed).unwrap();
assert_eq!(opened, b"hello xenia");
}
#[test]
fn nonce_counter_monotonic() {
let mut s = Session::new();
assert_eq!(s.next_nonce(), 0);
assert_eq!(s.next_nonce(), 1);
assert_eq!(s.next_nonce(), 2);
}
#[test]
fn nonce_counter_wraps_without_panic() {
let mut s = Session::new();
s.nonce_counter = u64::MAX;
assert_eq!(s.next_nonce(), u64::MAX);
assert_eq!(s.next_nonce(), 0);
}
#[test]
fn seal_refuses_at_sequence_exhaustion() {
let mut s = Session::with_source_id([0; 8], 0);
s.install_key([0x77; 32]);
s.nonce_counter = 1u64 << 32;
assert!(matches!(
s.seal(b"must-refuse", 0x10),
Err(WireError::SequenceExhausted)
));
}
#[test]
fn seal_allows_last_valid_sequence_before_exhaustion() {
let mut s = Session::with_source_id([0; 8], 0);
s.install_key([0x77; 32]);
s.nonce_counter = (1u64 << 32) - 1; let sealed = s.seal(b"last-valid", 0x10).expect("seal at boundary - 1");
assert_eq!(sealed.len(), 12 + 10 + 16); assert!(matches!(
s.seal(b"over-the-edge", 0x10),
Err(WireError::SequenceExhausted)
));
}
#[test]
fn rekey_resets_sequence_after_exhaustion() {
let mut s = Session::with_source_id([0; 8], 0);
s.install_key([0x77; 32]);
s.nonce_counter = 1u64 << 32;
assert!(s.seal(b"blocked", 0x10).is_err());
s.install_key([0x88; 32]);
assert!(s.seal(b"unblocked", 0x10).is_ok());
}
#[test]
fn rekey_preserves_old_envelopes_during_grace() {
let mut sender = Session::with_source_id([2; 8], 0xBB);
let mut receiver = Session::with_source_id([2; 8], 0xBB);
sender.install_key([0x44; 32]);
receiver.install_key([0x44; 32]);
let sealed_old = sender.seal(b"first", 0x10).unwrap();
receiver.install_key([0x55; 32]);
let opened = receiver.open(&sealed_old).unwrap();
assert_eq!(opened, b"first");
}
#[test]
fn replay_rejected() {
let mut sender = Session::with_source_id([3; 8], 0xCC);
let mut receiver = Session::with_source_id([3; 8], 0xCC);
sender.install_key([0x66; 32]);
receiver.install_key([0x66; 32]);
let sealed = sender.seal(b"once", 0x10).unwrap();
assert!(receiver.open(&sealed).is_ok());
assert!(matches!(receiver.open(&sealed), Err(WireError::OpenFailed)));
}
#[test]
fn wrong_key_fails() {
let mut sender = Session::with_source_id([4; 8], 0xDD);
let mut receiver = Session::with_source_id([4; 8], 0xDD);
sender.install_key([0x77; 32]);
receiver.install_key([0x88; 32]);
let sealed = sender.seal(b"secret", 0x10).unwrap();
assert!(matches!(receiver.open(&sealed), Err(WireError::OpenFailed)));
}
#[test]
fn independent_payload_types_do_not_collide() {
let mut sender = Session::with_source_id([5; 8], 0xEE);
let mut receiver = Session::with_source_id([5; 8], 0xEE);
sender.install_key([0x99; 32]);
receiver.install_key([0x99; 32]);
let a = sender.seal(b"frame-0", 0x10).unwrap();
let b = sender.seal(b"input-0", 0x11).unwrap();
assert!(receiver.open(&a).is_ok());
assert!(receiver.open(&b).is_ok());
}
#[test]
fn tick_expires_prev_key_after_grace() {
let mut sender = Session::with_source_id([6; 8], 0xFF);
let mut receiver =
Session::with_source_id([6; 8], 0xFF).with_rekey_grace(Duration::from_millis(1));
sender.install_key([0xAA; 32]);
receiver.install_key([0xAA; 32]);
let sealed_old = sender.seal(b"old", 0x10).unwrap();
receiver.install_key([0xBB; 32]);
std::thread::sleep(Duration::from_millis(5));
receiver.tick();
assert!(receiver.open(&sealed_old).is_err());
}
}