diff --git a/Cargo.toml b/Cargo.toml index a329c70..784eaaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ features = ["alloc"] optional = true [dependencies.smol-jsonrpc] -version = "0.1" +version = "0.2" optional = true [features] diff --git a/src/crc.rs b/src/crc.rs index 310a57a..b9ba1a2 100644 --- a/src/crc.rs +++ b/src/crc.rs @@ -23,7 +23,7 @@ pub fn crc16(data: &[u8]) -> u16 { if crc & 0x8000 != 0 { crc = (crc << 1) ^ CRC_POLY; } else { - crc = crc.overflowing_shl(1).0; + crc = saturating_shl(crc, 1); } } } @@ -31,6 +31,16 @@ pub fn crc16(data: &[u8]) -> u16 { crc } +fn saturating_shl(b: u16, s: u32) -> u16 { + let (res, o) = b.overflowing_shl(s); + + if o { + u16::MAX + } else { + res + } +} + #[cfg(test)] mod tests { use super::*; @@ -98,4 +108,46 @@ mod tests { ); assert_eq!(crc16(enc_data.as_ref()), 0x0000); } + + #[test] + fn test_sample() { + let data_crc = [0xac, 0x1a]; + let enc_data = [ + 0x00, + 0x11, + 0x7e, + 0xe5, + 0x65, + 0x07, + 0x0e, + 0x2a, + 0x8f, + 0xab, + 0xf7, + 0xdd, + 0xb3, + 0x87, + 0xe6, + 0x45, + 0xea, + 0xb2, + 0xbb, + data_crc[0], + data_crc[1], + ]; + assert_eq!( + crc16(enc_data[..enc_data.len() - 2].as_ref()), + u16::from_be_bytes(data_crc) + ); + + let enc_crc = [0x95, 0xfb]; + let enc_data = [ + 0x01, 0x05, 0x00, 0x00, 0x00, 0x07, 0x9c, 0x10, 0x69, 0xd9, 0x37, 0xf4, 0x18, 0x7f, + enc_crc[0], enc_crc[1], + ]; + assert_eq!( + crc16(enc_data[..enc_data.len() - 2].as_ref()), + u16::from_le_bytes(enc_crc) + ); + } } diff --git a/src/encrypted.rs b/src/encrypted.rs index fa3b4ae..f748901 100644 --- a/src/encrypted.rs +++ b/src/encrypted.rs @@ -31,6 +31,11 @@ pub fn sequence_count() -> SequenceCount { SEQUENCE_COUNT.load(Ordering::Relaxed).into() } +/// Sets the current [SequenceCount]. +pub fn set_sequence_count(count: u32) { + SEQUENCE_COUNT.store(count, Ordering::SeqCst); +} + /// Increments the [SequenceCount]. /// /// Returns the new [SequenceCount]. @@ -134,9 +139,9 @@ pub fn unstuff(buf: &mut [u8], mut end: usize) -> Result { Ok(end) } -#[cfg(test)] +#[cfg(all(test, feature = "std"))] pub mod tests { - use crate::{len, AesKey, MessageOps, Result}; + use crate::{len, std::sync::Mutex, AesKey, MessageOps, Result}; use super::*; @@ -145,6 +150,9 @@ pub mod tests { b'p', ]; + // Add a mutex to run tests one at a time + static T: Mutex<()> = Mutex::new(()); + fn test_key() -> AesKey { AesKey::clone_from_slice(TEST_KEY.as_ref()) } @@ -153,6 +161,8 @@ pub mod tests { fn test_command_encryption() -> Result<()> { use crate::PollCommand; + let _lock = T.lock(); + let key = test_key(); let mut poll_msg = PollCommand::new(); @@ -175,6 +185,8 @@ pub mod tests { fn test_response_encryption() -> Result<()> { use crate::{PollResponse, ResponseOps, ResponseStatus}; + let _lock = T.lock(); + let key = test_key(); let mut poll_msg = PollResponse::new(); @@ -197,6 +209,8 @@ pub mod tests { #[test] fn test_byte_stuffing() -> Result<()> { + let _lock = T.lock(); + let mut buf = [0x7f, 0xaa, 0xbb, 0x00]; let exp = [0x7f, 0x7f, 0xaa, 0xbb]; let end = 2; @@ -234,6 +248,8 @@ pub mod tests { #[test] fn test_byte_unstuffing() -> Result<()> { + let _lock = T.lock(); + let mut buf = [0x7f, 0x7f, 0xaa, 0xbb]; let exp = [0x7f, 0xaa, 0xbb, 0x00]; let end = buf.len() - 1; @@ -268,4 +284,71 @@ pub mod tests { Ok(()) } + + #[test] + fn test_encrypt_decrypt_stuffing() -> Result<()> { + use crate::PollCommand; + + let _lock = T.lock(); + + let mut msg = PollCommand::new(); + let _clear_csum = msg.calculate_checksum(); + + let key = AesKey::from([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, + ]); + + #[cfg(feature = "std")] + println!("Clear-text command buffer: {:x?}", msg.buf()); + + let mut enc_msg = EncryptedCommand::new(); + + enc_msg.set_message_data(&mut msg)?; + let wrap_msg = enc_msg.encrypt(&key); + + let _wrap_csum = wrap_msg.checksum(); + + #[cfg(feature = "std")] + println!("Wrapped encrypted command buffer: {:x?}", wrap_msg.buf()); + + let dec_msg = EncryptedResponse::decrypt(&key, wrap_msg); + + dec_msg.verify_checksum()?; + + Ok(()) + } + + #[test] + fn test_encrypt_known_keys() -> Result<()> { + use crate::{HostProtocolVersionCommand, ProtocolVersion}; + + let _lock = T.lock(); + + let key = AesKey::from([ + 0x67, 0x45, 0x23, 0x01, 0x67, 0x45, 0x23, 0x01, 0x5e, 0xfa, 0xe5, 0x0d, 0x00, 0x00, + 0x00, 0x00, + ]); + + let exp_enc_bytes = [ + 0x7f, 0x80, 0x11, 0x7e, 0x86, 0x01, 0xf3, 0xa7, 0x85, 0xec, 0x6f, 0x4b, 0x3d, 0x0f, + 0xf0, 0xfe, 0xd4, 0x8d, 0x16, 0x64, 0x2a, 0x23, + ]; + + let msg = HostProtocolVersionCommand::new().with_version(ProtocolVersion::Six); + + set_sequence_count(0); + + let mut wrap_msg = EncryptedCommand::new() + .with_count(0u32.into()) + .with_message_data(&msg)? + .encrypt(&key); + + wrap_msg.set_sequence_id(128u8.into()); + wrap_msg.calculate_checksum(); + + assert_eq!(wrap_msg.buf(), exp_enc_bytes.as_ref()); + + wrap_msg.verify_checksum() + } } diff --git a/src/encrypted/command.rs b/src/encrypted/command.rs index 83bc475..b8ba4e8 100644 --- a/src/encrypted/command.rs +++ b/src/encrypted/command.rs @@ -47,6 +47,17 @@ impl EncryptedCommand { self.buf[index::COUNT..index::COUNT_END].as_ref() } + fn set_count(&mut self, count: SequenceCount) { + self.buf[index::COUNT..index::COUNT_END] + .copy_from_slice(count.as_inner().to_le_bytes().as_ref()); + } + + /// Builder function that sets the [SequenceCount]. + pub fn with_count(mut self, count: SequenceCount) -> Self { + self.set_count(count); + self + } + /// Gets the message data. pub fn message_data(&self) -> &[u8] { let start = self.data_start(); @@ -75,22 +86,24 @@ impl EncryptedCommand { /// encrypted message is wrapped in an outer standard SSP message. /// /// Matryoshka dolls all the way down... - pub fn set_message_data(&mut self, message: &mut dyn CommandOps) -> Result<()> { + pub fn set_message_data(&mut self, message: &dyn CommandOps) -> Result<()> { let len = message.data_len(); if message.data().len() != len { return Err(Error::InvalidDataLength((len, message.data().len()))); } - message.calculate_checksum(); - if (0..=len::MAX_ENCRYPTED_DATA).contains(&len) { - self.buf[index::LEN] = len as u8; + self.set_data_len(len as u8); let start = self.data_start(); let end = self.data_end(); - self.buf[start..end].copy_from_slice(message.data()); + let data = message.data(); + + log::trace!("Encrypted data: {data:x?}, length: {len}"); + + self.buf[start..end].copy_from_slice(data); Ok(()) } else { @@ -98,6 +111,12 @@ impl EncryptedCommand { } } + /// Builder function that sets the message data. + pub fn with_message_data(mut self, message: &dyn CommandOps) -> Result { + self.set_message_data(message)?; + Ok(self) + } + /// Gets random packing data used to make the encrypted packet a mutliple of the [AES block /// length](crate::len::AES). /// @@ -133,7 +152,10 @@ impl EncryptedCommand { let start = self.packing_start(); let end = self.packing_end(); + #[cfg(not(test))] rng.fill_bytes(&mut self.buf[start..end]); + #[cfg(test)] + self.buf[start..end].copy_from_slice([0; 255][..end - start].as_ref()); } /// Adds random packing data to make the encrypted packet a mutliple of the [AES block @@ -155,7 +177,10 @@ impl EncryptedCommand { let start = self.packing_start(); let end = self.packing_end(); + #[cfg(not(test))] rng.fill_bytes(&mut self.buf[start..end]); + #[cfg(test)] + self.buf[start..end].copy_from_slice([0; 255][..end - start].as_ref()); } fn packing_start(&self) -> usize { @@ -185,9 +210,15 @@ impl EncryptedCommand { /// /// Converts the [EncryptedCommand] message into a standard [WrappedEncryptedMessage]. pub fn encrypt(mut self, key: &AesKey) -> WrappedEncryptedMessage { - use crate::aes; + //use crate::aes; + use aes::cipher::{BlockEncrypt, KeyInit}; self.set_packing(); + + if super::sequence_count().as_inner() != 0 { + self.set_count(super::sequence_count()); + } + self.calculate_checksum(); let mut enc_msg = WrappedEncryptedMessage::new(); @@ -195,20 +226,36 @@ impl EncryptedCommand { let enc_len = self.len(); enc_msg.set_data_len(enc_len as u8); + log::trace!("Encrypted message: {:x?}", self.buf()); + let plain_data = self.encrypt_data(); let cipher_data = enc_msg.data_mut()[1..].as_mut(); - if let Err(err) = aes::aes_encrypt_inplace(key.as_ref(), plain_data, cipher_data) { - log::error!("error encrypting message: {err}"); + let ciph = aes::Aes128::new(key); + + for (pchunk, cchunk) in plain_data + .chunks_exact(16) + .zip(cipher_data.chunks_exact_mut(16)) + { + ciph.encrypt_block_b2b(pchunk.into(), cchunk.into()); } - super::increment_sequence_count(); - log::trace!("encryption sequence count: {}", super::sequence_count()); + enc_msg.calculate_checksum(); + if let Err(err) = enc_msg.verify_checksum() { + log::error!("error validating wrapped encrypted checksum: {err}"); + } if let Err(err) = enc_msg.stuff_encrypted_data() { log::error!("error stuffing encrypted command message: {err}"); } + log::trace!("encryption sequence count: {}", super::sequence_count()); + #[cfg(any(not(test), feature = "test-crypto"))] + log::trace!( + "next encryption sequence count: {}", + super::increment_sequence_count() + ); + enc_msg } diff --git a/src/encrypted/response.rs b/src/encrypted/response.rs index 5716bb0..e61b576 100644 --- a/src/encrypted/response.rs +++ b/src/encrypted/response.rs @@ -233,8 +233,15 @@ impl EncryptedResponse { log::error!("error decrypting response message: {err}"); } - super::increment_sequence_count(); - log::trace!("decryption sequence count: {}", super::sequence_count()); + log::trace!("decrypted data: {:x?}", plain_data); + + let seq_count = super::sequence_count(); + let dec_count = dec_msg.count(); + + if seq_count != dec_count { + log::error!("decryption sequence count is out of sync: have: {dec_count}, expected: {seq_count}"); + super::set_sequence_count(dec_count.as_inner()); + } dec_msg } diff --git a/src/encrypted/wrapped.rs b/src/encrypted/wrapped.rs index 56d93e8..9cd377b 100644 --- a/src/encrypted/wrapped.rs +++ b/src/encrypted/wrapped.rs @@ -10,6 +10,7 @@ use crate::{ #[derive(Clone, Debug, PartialEq, zeroize::Zeroize, zeroize::ZeroizeOnDrop)] pub struct WrappedEncryptedMessage { buf: [u8; len::WRAPPED_ENCRYPTED_MESSAGE], + stuffing: usize, } impl WrappedEncryptedMessage { @@ -17,6 +18,7 @@ impl WrappedEncryptedMessage { pub fn new() -> Self { let mut msg = Self { buf: [0u8; len::WRAPPED_ENCRYPTED_MESSAGE], + stuffing: 0, }; msg.init(); @@ -40,9 +42,14 @@ impl WrappedEncryptedMessage { self.buf[start..end].as_ref() } + /// Gets whether the [WrappedEncryptedMessage] contains byte stuffing (repeated `0x7f` bytes). + pub fn is_stuffed(&self) -> bool { + self.stuffing != 0 + } + /// Adds byte stuffing to the encrypted message data to avoid devices thinking we started a new /// packet in the middle of an encrypted packet. - pub(crate) fn stuff_encrypted_data(&mut self) -> Result { + pub fn stuff_encrypted_data(&mut self) -> Result { use super::stuff; use crate::message::index; @@ -50,11 +57,14 @@ impl WrappedEncryptedMessage { // end after the CRC data let end = start + self.data_len() + 2; - stuff(&mut self.buf[start..], end) + let new_end = stuff(&mut self.buf[start..], end)?; + self.stuffing = new_end.saturating_sub(end); + + Ok(self.stuffing) } /// Removes byte stuffing from the encrypted message data. - pub(crate) fn unstuff_encrypted_data(&mut self) -> Result<()> { + pub fn unstuff_encrypted_data(&mut self) -> Result<()> { use super::unstuff; use crate::message::{index, STX}; @@ -81,6 +91,7 @@ impl WrappedEncryptedMessage { if calc_end != exp_end { Err(Error::InvalidLength((calc_end, exp_end))) } else { + self.stuffing = 0; Ok(()) } } diff --git a/src/host_protocol_version/command.rs b/src/host_protocol_version/command.rs index 1bc7f9b..16837ad 100644 --- a/src/host_protocol_version/command.rs +++ b/src/host_protocol_version/command.rs @@ -38,6 +38,12 @@ impl HostProtocolVersionCommand { pub fn set_version(&mut self, version: ProtocolVersion) { self.buf[index::PROTOCOL_VERSION] = version.into(); } + + /// Builder function that sets the [ProtocolVersion]. + pub fn with_version(mut self, version: ProtocolVersion) -> Self { + self.set_version(version); + self + } } impl_default!(HostProtocolVersionCommand); diff --git a/src/keys.rs b/src/keys.rs index 2ec6c37..ae278f6 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -219,6 +219,11 @@ mod tests { let exp_fixed_key = DEFAULT_FIXED_KEY_U64.to_le_bytes(); assert_eq!(DEFAULT_FIXED_KEY[..8].as_ref(), exp_fixed_key.as_ref()); + + assert_eq!( + AesKey::from(FixedKey::from_inner(DEFAULT_FIXED_KEY_U64))[..8].as_ref(), + [0x67, 0x45, 0x23, 0x01, 0x67, 0x45, 0x23, 0x01].as_ref(), + ); } #[test] @@ -258,5 +263,14 @@ mod tests { EncryptionKey::from_keys(&dev_inter, &host_rnd, &modulus), encrypt_key ); + + let dev_inter = IntermediateKey::from_inner(0x04ba466d); + let host_rnd = RandomKey::from_inner(0x2d61283d); + let modulus = ModulusKey::from_inner(0x2d469703); + + assert_eq!( + EncryptionKey::from_keys(&dev_inter, &host_rnd, &modulus), + EncryptionKey::from_inner(0x1aeda1fb), + ); } } diff --git a/src/macros.rs b/src/macros.rs index 1ffb10f..7826397 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -591,12 +591,6 @@ macro_rules! impl_encrypted_message_ops { fn init(&mut self) { use $crate::encrypted::encrypted_index as index; self.buf[index::STEX] = $crate::encrypted::STEX; - - let count_start = index::COUNT; - let count_end = index::COUNT_END; - let count = $crate::encrypted::sequence_count().as_inner().to_be_bytes(); - - self.buf[count_start..count_end].copy_from_slice(count.as_ref()); } fn buf(&self) -> &[u8] { @@ -776,18 +770,31 @@ macro_rules! impl_wrapped_message_ops { self.buf[index::STX] = $crate::STX; self.buf[index::SEQ_ID] = $crate::SequenceId::new().into(); self.buf[index::LEN] = data_len as u8; + self.buf[index::DATA] = $crate::STEX; } fn buf(&self) -> &[u8] { - let len = self.data_len() + self.metadata_len(); + let len = self.data_len() + self.metadata_len() + self.stuffing; self.buf[..len].as_ref() } fn buf_mut(&mut self) -> &mut [u8] { - let len = self.data_len() + self.metadata_len(); + let len = self.data_len() + self.metadata_len() + self.stuffing; self.buf[..len].as_mut() } + fn as_bytes(&mut self) -> &[u8] { + // don't calculate the checksum here, there may be byte stuffing + // checksum is calculated when the message is encrypted and wrapped + self.buf() + } + + fn as_bytes_mut(&mut self) -> &mut [u8] { + // don't calculate the checksum here, there may be byte stuffing + // checksum is calculated when the message is encrypted and wrapped + self.buf_mut() + } + fn data_len(&self) -> usize { use $crate::message::index; let inited = self.buf[index::LEN] != 0; diff --git a/src/message.rs b/src/message.rs index b5b1692..7f7c122 100644 --- a/src/message.rs +++ b/src/message.rs @@ -240,7 +240,7 @@ pub trait MessageOps { let buf_crc = u16::from_le_bytes(buf[buf_len - 2..].try_into().unwrap()); let exp_crc = crc16(buf[index::SEQ_ID..buf_len - 2].as_ref()); - if buf_crc != exp_crc { + if buf_crc != exp_crc && msg_type != MessageType::Encrypted { return Err(Error::Crc((buf_crc, exp_crc))); } diff --git a/src/primes.rs b/src/primes.rs index 1408b93..5603cf9 100644 --- a/src/primes.rs +++ b/src/primes.rs @@ -59,7 +59,9 @@ impl Generator { /// Generates a new prime. pub fn new_prime(&mut self) -> u64 { loop { - let candidate = self.rng.next_u64(); + // all primes generated by the SDK examples are 32-bit + // exponentially decreasing any security... + let candidate = self.rng.next_u32() as u64; if Self::is_prime(candidate, &mut self.rng) { return candidate; diff --git a/src/types/response_status.rs b/src/types/response_status.rs index 52e2ada..3ad2b16 100644 --- a/src/types/response_status.rs +++ b/src/types/response_status.rs @@ -115,6 +115,8 @@ pub enum ResponseStatus { /// The device is in encrypted communication mode, but the encryption keys have not been /// negotiated. KeyNotSet = 0xfa, + /// The device has had all its note channels inhibited, and has become disabled for note insertion. + ChannelDisable = 0xb5, /// Reserved for future use Reserved(u8), } @@ -155,6 +157,7 @@ impl ResponseStatus { 0xf5 => Self::CommandCannotBeProcessed, 0xf8 => Self::Fail, 0xfa => Self::KeyNotSet, + 0xb5 => Self::ChannelDisable, res => Self::Reserved(res), } } @@ -184,6 +187,7 @@ impl ResponseStatus { Self::CommandCannotBeProcessed => 0xf5, Self::Fail => 0xf8, Self::KeyNotSet => 0xfa, + Self::ChannelDisable => 0xb5, Self::Reserved(res) => *res, } } @@ -197,31 +201,7 @@ impl From for ResponseStatus { impl From for u8 { fn from(val: ResponseStatus) -> Self { - match val { - ResponseStatus::NoteClearedFromFront => 0xe1, - ResponseStatus::NoteClearedIntoCashbox => 0xe2, - ResponseStatus::CashboxRemoved => 0xe3, - ResponseStatus::CashboxReplaced => 0xe4, - ResponseStatus::FraudAttempt => 0xe6, - ResponseStatus::StackerFull => 0xe7, - ResponseStatus::Disabled => 0xe8, - ResponseStatus::UnsafeJam => 0xe9, - ResponseStatus::Stacked => 0xeb, - ResponseStatus::Stacking => 0xcc, - ResponseStatus::Rejected => 0xec, - ResponseStatus::Rejecting => 0xed, - ResponseStatus::NoteCredit => 0xee, - ResponseStatus::Read => 0xef, - ResponseStatus::Ok => 0xf0, - ResponseStatus::DeviceReset => 0xf1, - ResponseStatus::CommandNotKnown => 0xf2, - ResponseStatus::WrongNumberParameters => 0xf3, - ResponseStatus::ParameterOutOfRange => 0xf4, - ResponseStatus::CommandCannotBeProcessed => 0xf5, - ResponseStatus::Fail => 0xf8, - ResponseStatus::KeyNotSet => 0xfa, - ResponseStatus::Reserved(s) => s, - } + val.to_u8() } } @@ -256,6 +236,7 @@ impl From for &'static str { ResponseStatus::CommandCannotBeProcessed => "CommandCannotBeProcessed", ResponseStatus::Fail => "Fail", ResponseStatus::KeyNotSet => "KeyNotSet", + ResponseStatus::ChannelDisable => "ChannelDisable", ResponseStatus::Reserved(_) => "Reserved", } } @@ -271,7 +252,7 @@ impl fmt::Display for ResponseStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Reserved(s) => write!(f, "Reserved(0x{s:02x})"), - _ => write!(f, "{}", <&'static str>::from(self)), + _ => write!(f, "{}", <&str>::from(self)), } } }