use std::sync::Arc;
use std::time::Duration;
use net::adapter::net::channel::ChannelName;
pub use net::adapter::net::identity::{
EntityError, EntityId, EntityKeypair, OriginStamp, PermissionToken, TokenCache, TokenError,
TokenScope,
};
#[derive(Clone, Debug)]
pub struct Identity {
keypair: Arc<EntityKeypair>,
cache: Arc<TokenCache>,
}
impl Identity {
pub fn generate() -> Self {
Self::from_keypair(EntityKeypair::generate())
}
pub fn from_seed(seed: [u8; 32]) -> Self {
Self::from_keypair(EntityKeypair::from_bytes(seed))
}
pub fn to_bytes(&self) -> [u8; 32] {
*self.keypair.secret_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, TokenError> {
if bytes.len() != 32 {
return Err(TokenError::InvalidFormat);
}
let mut seed = [0u8; 32];
seed.copy_from_slice(bytes);
Ok(Self::from_seed(seed))
}
pub fn entity_id(&self) -> &EntityId {
self.keypair.entity_id()
}
pub fn origin_hash(&self) -> u64 {
self.keypair.origin_hash()
}
pub fn node_id(&self) -> u64 {
self.keypair.node_id()
}
pub fn sign(&self, message: &[u8]) -> [u8; 64] {
self.keypair.sign(message).to_bytes()
}
pub fn issue_token(
&self,
subject: EntityId,
scope: TokenScope,
channel: &ChannelName,
ttl: Duration,
delegation_depth: u8,
) -> PermissionToken {
debug_assert!(
!ttl.is_zero(),
"Identity::issue_token called with Duration::ZERO; \
release builds soft-clamp to 1s, but the call site is likely a bug"
);
let effective_ttl = if ttl.is_zero() {
Duration::from_secs(1)
} else {
ttl
};
self.try_issue_token(subject, scope, channel, effective_ttl, delegation_depth)
.expect("Identity::issue_token: invalid input (use try_issue_token for fallible)")
}
pub fn try_issue_token(
&self,
subject: EntityId,
scope: TokenScope,
channel: &ChannelName,
ttl: Duration,
delegation_depth: u8,
) -> Result<PermissionToken, TokenError> {
PermissionToken::try_issue(
&self.keypair,
subject,
scope,
channel.hash(),
ttl.as_secs(),
delegation_depth,
)
}
pub fn install_token(&self, token: PermissionToken) -> Result<(), TokenError> {
self.cache.insert(token)
}
pub fn lookup_token(
&self,
subject: &EntityId,
channel: &ChannelName,
) -> Option<PermissionToken> {
self.cache.get(subject, channel.hash())
}
pub fn keypair(&self) -> &Arc<EntityKeypair> {
&self.keypair
}
pub fn token_cache(&self) -> &Arc<TokenCache> {
&self.cache
}
fn from_keypair(kp: EntityKeypair) -> Self {
Self {
keypair: Arc::new(kp),
cache: Arc::new(TokenCache::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(debug_assertions))]
#[test]
fn issue_token_zero_duration_soft_clamps_in_release() {
let id = Identity::generate();
let subject = Identity::generate();
let channel = ChannelName::new("zero-ttl-soft-clamp").unwrap();
let token = id.issue_token(
subject.entity_id().clone(),
crate::TokenScope::PUBLISH,
&channel,
Duration::ZERO,
0,
);
assert!(
token.verify().is_ok(),
"soft-clamped 1s TTL must produce a verify-ok token"
);
assert!(
token.is_valid().is_ok(),
"soft-clamped 1s TTL must be live at issue time"
);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "Duration::ZERO")]
fn issue_token_zero_duration_debug_asserts() {
let id = Identity::generate();
let subject = Identity::generate();
let channel = ChannelName::new("zero-ttl-debug").unwrap();
let _ = id.issue_token(
subject.entity_id().clone(),
crate::TokenScope::PUBLISH,
&channel,
Duration::ZERO,
0,
);
}
#[test]
fn try_issue_token_zero_duration_returns_zero_ttl() {
let id = Identity::generate();
let subject = Identity::generate();
let channel = ChannelName::new("zero-ttl-fallible").unwrap();
let err = id
.try_issue_token(
subject.entity_id().clone(),
crate::TokenScope::PUBLISH,
&channel,
Duration::ZERO,
0,
)
.unwrap_err();
assert!(
matches!(err, TokenError::ZeroTtl),
"expected ZeroTtl, got {err:?}"
);
}
}