use core::num::NonZeroU8;
use crate::crypto::CanonPkcSharedSecret;
use crate::error::Error;
use crate::fabric::MAX_FABRICS;
use crate::persist::{KvBlobStore, CASE_RESUMPTION_KEY, KV_BUF_SIZE};
use crate::tlv::{FromTLV, TLVElement, TLVTag, ToTLV};
use crate::transport::session::{NocCatIds, MAX_SESSIONS};
use crate::utils::init::{init, Init};
use crate::utils::storage::{Vec, WriteBuf};
use super::casep::CaseResumptionId;
const fn min(a: usize, b: usize) -> usize {
if a < b {
a
} else {
b
}
}
const RESUMPTION_RECORD_TLV_MAX: usize = 128;
const RESUMPTION_ENVELOPE_TLV_MAX: usize = 16;
const KV_BUDGET_CAP: usize = if KV_BUF_SIZE > RESUMPTION_ENVELOPE_TLV_MAX {
(KV_BUF_SIZE - RESUMPTION_ENVELOPE_TLV_MAX) / RESUMPTION_RECORD_TLV_MAX
} else {
0
};
pub const MAX_RESUMPTION_RECORDS: usize =
min(min(min(3 * MAX_FABRICS, MAX_SESSIONS), 16), KV_BUDGET_CAP);
#[derive(Debug, Clone, FromTLV, ToTLV)]
pub struct ResumableSession {
pub fab_idx: NonZeroU8,
pub peer_nodeid: u64,
pub peer_cat_ids: NocCatIds,
pub resumption_id: CaseResumptionId,
pub shared_secret: CanonPkcSharedSecret,
}
impl ResumableSession {
pub fn is_for_peer(&self, fab_idx: NonZeroU8, peer_nodeid: u64) -> bool {
self.fab_idx == fab_idx && self.peer_nodeid == peer_nodeid
}
pub fn has_resumption_id(&self, id: &[u8]) -> bool {
self.resumption_id.reference().access().as_slice() == id
}
}
pub struct ResumableSessions {
records: Vec<ResumableSession, MAX_RESUMPTION_RECORDS>,
}
impl ResumableSessions {
#[inline(always)]
pub const fn new() -> Self {
Self {
records: Vec::new(),
}
}
pub fn init() -> impl Init<Self> {
init!(Self {
records <- Vec::init(),
})
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ResumableSession> {
self.records.iter()
}
pub fn reset(&mut self) {
self.records.clear();
}
pub fn find_by_resumption_id(&self, resumption_id: &[u8]) -> Option<&ResumableSession> {
self.records
.iter()
.find(|r| r.has_resumption_id(resumption_id))
}
pub fn find_by_peer(&self, fab_idx: NonZeroU8, peer_nodeid: u64) -> Option<&ResumableSession> {
self.records
.iter()
.find(|r| r.is_for_peer(fab_idx, peer_nodeid))
}
pub fn insert_or_update(&mut self, record: ResumableSession) {
if MAX_RESUMPTION_RECORDS == 0 {
return;
}
let key = (record.fab_idx, record.peer_nodeid);
self.records.retain(|r| (r.fab_idx, r.peer_nodeid) != key);
if self.records.is_full() {
self.records.remove(0);
}
let _ = self.records.push(record);
}
pub fn remove_for_fabric(&mut self, fab_idx: NonZeroU8) {
self.records.retain(|r| r.fab_idx != fab_idx);
}
pub fn remove_by_peer(&mut self, fab_idx: NonZeroU8, peer_nodeid: u64) {
self.records
.retain(|r| !r.is_for_peer(fab_idx, peer_nodeid));
}
pub fn load_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
let parse_result: Result<Vec<ResumableSession, MAX_RESUMPTION_RECORDS>, Error> = {
let Some(data) = store.load(CASE_RESUMPTION_KEY, buf)? else {
return Ok(());
};
Vec::<ResumableSession, MAX_RESUMPTION_RECORDS>::from_tlv(&TLVElement::new(data))
};
match parse_result {
Ok(records) => {
self.records = records;
info!(
"Loaded {} CASE session resumption record(s) from storage",
self.records.len()
);
Ok(())
}
Err(e) => {
warn!(
"CASE session resumption cache is unparseable ({}); \
dropping the persisted blob so the node can still boot",
e
);
let _ = store.remove(CASE_RESUMPTION_KEY, buf);
Ok(())
}
}
}
pub fn store_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
let len = {
let mut wb = WriteBuf::new(buf);
self.records.to_tlv(&TLVTag::Anonymous, &mut wb)?;
wb.get_tail()
};
let (data, scratch) = buf.split_at_mut(len);
store.store(CASE_RESUMPTION_KEY, data, scratch)
}
pub fn reset_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
store.remove(CASE_RESUMPTION_KEY, buf)?;
info!("Removed CASE session resumption cache from storage");
Ok(())
}
}
impl Default for ResumableSessions {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use core::num::NonZeroU8;
use crate::crypto::CanonPkcSharedSecret;
use super::super::casep::CaseResumptionId;
use super::{ResumableSession, ResumableSessions, MAX_RESUMPTION_RECORDS};
fn rec(fab: u8, nodeid: u64, rid_first_byte: u8) -> ResumableSession {
let mut resumption_id = CaseResumptionId::new();
resumption_id.access_mut()[0] = rid_first_byte;
ResumableSession {
fab_idx: NonZeroU8::new(fab).unwrap(),
peer_nodeid: nodeid,
peer_cat_ids: [0; 3],
resumption_id,
shared_secret: CanonPkcSharedSecret::new(),
}
}
#[test]
fn insert_moves_to_tail_and_dedupes_by_peer() {
let mut cache = ResumableSessions::new();
cache.insert_or_update(rec(1, 100, 0xA0));
cache.insert_or_update(rec(1, 200, 0xB0));
assert_eq!(cache.len(), 2);
cache.insert_or_update(rec(1, 100, 0xC0));
assert_eq!(cache.len(), 2);
let ids: heapless::Vec<u8, 4> = cache
.iter()
.map(|r| r.resumption_id.reference().access()[0])
.collect();
assert_eq!(ids.as_slice(), &[0xB0, 0xC0]);
}
#[test]
fn find_by_peer_and_by_resumption_id() {
if MAX_RESUMPTION_RECORDS < 2 {
return; }
let mut cache = ResumableSessions::new();
cache.insert_or_update(rec(1, 100, 0xA0));
cache.insert_or_update(rec(2, 200, 0xB0));
assert!(cache
.find_by_peer(NonZeroU8::new(1).unwrap(), 100)
.is_some());
assert!(cache
.find_by_peer(NonZeroU8::new(2).unwrap(), 100)
.is_none());
let mut needle = [0u8; 16];
needle[0] = 0xB0;
assert!(cache.find_by_resumption_id(&needle).is_some());
needle[0] = 0xFF;
assert!(cache.find_by_resumption_id(&needle).is_none());
assert!(cache.find_by_resumption_id(&[0xB0; 8]).is_none());
}
#[test]
fn remove_for_fabric_drops_only_that_fabric() {
if MAX_RESUMPTION_RECORDS < 3 {
return;
}
let mut cache = ResumableSessions::new();
cache.insert_or_update(rec(1, 100, 0xA0));
cache.insert_or_update(rec(2, 100, 0xB0));
cache.insert_or_update(rec(1, 200, 0xC0));
assert_eq!(cache.len(), 3);
cache.remove_for_fabric(NonZeroU8::new(1).unwrap());
assert_eq!(cache.len(), 1);
assert!(cache
.find_by_peer(NonZeroU8::new(2).unwrap(), 100)
.is_some());
}
}