use std::{
panic::{self, AssertUnwindSafe},
time::{Duration, Instant},
};
use subtle::ConstantTimeEq;
use crate::command::{Command, CommandCode, CommandMessage};
use crate::connector::{Connection, Connector};
use crate::credentials::Credentials;
use crate::error::HsmErrorKind;
use crate::response::ResponseMessage;
use crate::serialization::deserialize;
#[macro_use]
mod macros;
pub(crate) mod command;
mod error;
mod id;
pub(crate) mod securechannel;
mod timeout;
use self::command::close::*;
pub use self::id::SessionId;
use self::securechannel::{Challenge, SecureChannel};
use self::SessionErrorKind::*;
pub use self::{
error::{SessionError, SessionErrorKind},
timeout::SessionTimeout,
};
const TIMEOUT_FUZZ_FACTOR: Duration = Duration::from_secs(1);
pub struct Session {
id: SessionId,
connection: Box<dyn Connection>,
secure_channel: Option<SecureChannel>,
created_at: Instant,
last_active: Instant,
timeout: SessionTimeout,
}
impl Session {
pub(super) fn open(
connector: &dyn Connector,
credentials: &Credentials,
timeout: SessionTimeout,
) -> Result<Self, SessionError> {
ensure!(
timeout.duration() > TIMEOUT_FUZZ_FACTOR,
CreateFailed,
"timeout too low: must be longer than {:?}",
TIMEOUT_FUZZ_FACTOR
);
connector.healthcheck()?;
let connection = connector.connect()?;
let host_challenge = Challenge::random();
let (session_id, session_response) = command::create_session(
&*connection,
credentials.authentication_key_id,
host_challenge,
)?;
let channel = SecureChannel::new(
session_id,
&credentials.authentication_key,
host_challenge,
session_response.card_challenge,
);
if channel
.card_cryptogram()
.ct_eq(&session_response.card_cryptogram)
.unwrap_u8()
!= 1
{
fail!(
AuthFail,
"(session: {}) card cryptogram mismatch!",
channel.id().to_u8()
);
}
let id = channel.id();
let now = Instant::now();
let mut session = Session {
id,
connection,
secure_channel: Some(channel),
created_at: now,
last_active: now,
timeout,
};
session.authenticate(credentials)?;
Ok(session)
}
pub fn is_open(&self) -> bool {
self.secure_channel.is_some() && !self.is_timed_out()
}
pub fn id(&self) -> SessionId {
self.id
}
pub fn duration(&self) -> Duration {
Instant::now().duration_since(self.created_at)
}
pub fn messages_sent(&self) -> Result<usize, SessionError> {
self.secure_channel
.as_ref()
.ok_or_else(|| err!(ClosedSessionError, "session is already closed"))
.map(SecureChannel::counter)
}
pub fn is_timed_out(&self) -> bool {
let idle_time = Instant::now().duration_since(self.last_active);
let timeout_with_fuzz = self.timeout.duration() - TIMEOUT_FUZZ_FACTOR;
idle_time >= timeout_with_fuzz
}
pub(crate) fn send_command<C: Command>(
&mut self,
command: C,
) -> Result<C::ResponseType, SessionError> {
let plaintext_cmd: CommandMessage = command.into();
let cmd_type = plaintext_cmd.command_type;
let encrypted_cmd = self
.secure_channel()?
.encrypt_command(plaintext_cmd)
.map_err(|e| {
self.secure_channel = None;
e
})?;
let uuid = encrypted_cmd.uuid;
session_debug!(
self,
"n={} uuid={} cmd={:?}",
self.messages_sent()?,
uuid,
C::COMMAND_CODE
);
let encrypted_response = self.send_message(encrypted_cmd)?;
let response = self
.secure_channel()?
.decrypt_response(encrypted_response)
.map_err(|e| {
self.secure_channel = None;
e
})?;
if response.is_err() {
if let Some(kind) = HsmErrorKind::from_response_message(&response) {
session_debug!(self, "uuid={} failed={:?} error={:?}", uuid, cmd_type, kind);
return Err(kind.into());
} else {
session_debug!(self, "uuid={} failed={:?} error=unknown", uuid, cmd_type);
fail!(ResponseError, "{:?} failed: HSM error", cmd_type);
}
}
if response.command() != Some(C::COMMAND_CODE) {
fail!(
ResponseError,
"bad command type in response: {:?} (expected {:?})",
response.command(),
C::COMMAND_CODE,
);
}
deserialize(response.data.as_ref()).map_err(|e| e.into())
}
fn send_message(&mut self, cmd: CommandMessage) -> Result<ResponseMessage, SessionError> {
let cmd_type = cmd.command_type;
let uuid = cmd.uuid;
self.last_active = Instant::now();
if cmd_type != CommandCode::SessionMessage {
session_debug!(
self,
"n={} uuid={} msg={:?}",
self.messages_sent()?,
&uuid,
cmd_type
);
}
let response = match self.connection.send_message(uuid, cmd.into()) {
Ok(response_bytes) => ResponseMessage::parse(response_bytes)?,
Err(e) => {
self.secure_channel = None;
return Err(e.into());
}
};
if response.is_err() {
session_error!(self, "uuid={} error={:?}", &uuid, response.code);
fail!(ResponseError, "HSM error (session: {})", self.id().to_u8(),);
}
Ok(response)
}
fn authenticate(&mut self, credentials: &Credentials) -> Result<(), SessionError> {
session_debug!(
self,
"command={:?} key={}",
CommandCode::AuthenticateSession,
credentials.authentication_key_id
);
let command = self.secure_channel()?.authenticate_session()?;
let response = self.send_message(command)?;
if let Err(e) = self
.secure_channel()?
.finish_authenticate_session(&response)
{
session_error!(
self,
"failed={:?} key={} err={:?}",
CommandCode::AuthenticateSession,
credentials.authentication_key_id,
e.to_string()
);
return Err(e);
}
session_debug!(self, "auth=OK key={}", credentials.authentication_key_id);
Ok(())
}
fn secure_channel(&mut self) -> Result<&mut SecureChannel, SessionError> {
self.secure_channel
.as_mut()
.ok_or_else(|| err!(ClosedSessionError, "session is already closed"))
}
}
impl Drop for Session {
fn drop(&mut self) {
if self.is_timed_out() {
return;
}
session_debug!(self, "closing dropped session");
close_session(self);
}
}
fn close_session(session: &mut Session) {
let err = match panic::catch_unwind(AssertUnwindSafe(|| {
session.send_command(CloseSessionCommand {}).unwrap()
})) {
Ok(_) => return,
Err(e) => e,
};
let msg = err
.downcast_ref::<String>()
.map(|m| m.as_ref())
.or_else(|| err.downcast_ref::<&str>().cloned())
.unwrap_or_else(|| "unknown cause!");
error!(
"session={} panic closing dropped session: {}",
session.id().to_u8(),
msg
);
}