From 649341eb0fc53359717e2b8c1de9b9bfdfb01efe Mon Sep 17 00:00:00 2001 From: Holger Rapp Date: Fri, 15 Dec 2023 21:12:25 +0100 Subject: [PATCH] Parse more ReadCard replies (#13) Also: - bug fix: the terminal accepts LLV encoded values only if they have have a `f` has high nibble, so `0xf5` instead of `0x05`. - better debug logs to figure out what is actually going on. - enable debug output in tests. --- Cargo.lock | 64 ++++++++++++++++ zvt/BUILD.bazel | 2 + zvt/Cargo.toml | 4 + zvt/data/status_information_read_card.blob | Bin 0 -> 137 bytes zvt/src/feig/packets/mod.rs | 17 +++-- zvt/src/packets.rs | 82 +++++++++++++++++---- zvt/src/packets/tlv.rs | 32 ++++++-- zvt/tests/derive.rs | 2 +- zvt_builder/src/length.rs | 2 +- zvt_cli/src/main.rs | 6 ++ zvt_derive/src/lib.rs | 9 ++- 11 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 zvt/data/status_information_read_card.blob diff --git a/Cargo.lock b/Cargo.lock index 9a71682..194b1a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.29" @@ -292,6 +298,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -502,12 +514,56 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.26" @@ -527,6 +583,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + [[package]] name = "serde" version = "1.0.193" @@ -910,9 +972,11 @@ dependencies = [ "anyhow", "async-stream", "chrono", + "env_logger", "futures", "log", "pretty-hex", + "rstest", "tokio", "tokio-stream", "zvt_builder", diff --git a/zvt/BUILD.bazel b/zvt/BUILD.bazel index bb1db49..d8314af 100644 --- a/zvt/BUILD.bazel +++ b/zvt/BUILD.bazel @@ -17,4 +17,6 @@ rust_test( crate = ":zvt", data = glob(["data/*.blob"]), edition = "2021", + proc_macro_deps = all_crate_deps(proc_macro = True), + deps = all_crate_deps(normal_dev = True), ) diff --git a/zvt/Cargo.toml b/zvt/Cargo.toml index 8312706..d6afdb3 100644 --- a/zvt/Cargo.toml +++ b/zvt/Cargo.toml @@ -11,6 +11,10 @@ description = """ A crate to interact with payment terminals (ECRs) that use the ZVT protocol, including stand alone commandline tools to interact with the devices. """ +[dev-dependencies] +rstest = "0.18.2" +env_logger = "0.10.1" + [dependencies] anyhow = "1.0.70" async-stream = "0.3.5" diff --git a/zvt/data/status_information_read_card.blob b/zvt/data/status_information_read_card.blob new file mode 100644 index 0000000000000000000000000000000000000000..b45c5b93728c026af19569d9e69f930b7fb895f2 GIT binary patch literal 137 zcmZSKZ&PPb{`fOpb%Ki^g8+kwu8NR?kVS%U8$Vl)JU1Hy0|O%igS?1, - #[zvt_bmp(number = 0x27)] + #[zvt_bmp(number = 0x23, length = length::Llv, encoding= encoding::Hex)] + pub track_2_data: Option, + + #[zvt_bmp(number = 0x27, length = length::Fixed<1>)] pub result_code: Option, #[zvt_bmp(number = 0x29, length = length::Fixed<4>, encoding = encoding::Bcd)] @@ -249,6 +252,9 @@ pub struct Reservation { #[zvt_bmp(number = 0x22, length = length::Llv, encoding = encoding::Bcd)] pub card_number: Option, + #[zvt_bmp(number = 0x23, length = length::Llv, encoding= encoding::Hex)] + pub track_2_data: Option, + // Unclear how to interpret this. #[zvt_bmp(number = 0x01)] pub timeout: Option, @@ -425,6 +431,13 @@ pub mod tests { use chrono::NaiveDate; use std::fs; + #[rstest::fixture] + pub fn common_setup() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")) + .is_test(true) + .init(); + } + pub fn get_bytes(name: &str) -> Vec { let path_from_root = "zvt/data/".to_string(); let base_dir = match fs::metadata(&path_from_root) { @@ -434,7 +447,7 @@ pub mod tests { fs::read(&format!("{base_dir}/{name}")).unwrap() } - #[test] + #[rstest::rstest] fn test_read_card() { let bytes = get_bytes("1680722649.972316000_ecr_pt.blob"); let expected = ReadCard { @@ -452,7 +465,7 @@ pub mod tests { assert_eq!(expected, output.0); } - #[test] + #[rstest::rstest] fn test_status_information() { let bytes = get_bytes("1680728161.963129000_pt_ecr.blob"); let expected = StatusInformation { @@ -461,11 +474,15 @@ pub mod tests { uuid: Some("000000000000081ca72f".to_string()), ats: Some("0578807002".to_string()), card_type: Some(1), + maximum_pre_autorisation: None, + card_identification_item: None, + subs_on_card: None, sub_type: Some("fe04".to_string()), atqa: Some("0400".to_string()), sak: Some(0x20), subs: vec![tlv::Subs { application_id: Some("a0000000041010".to_string()), + card_type: None, }], }), ..StatusInformation::default() @@ -596,7 +613,44 @@ pub mod tests { assert!(expected.tlv.as_ref().unwrap().subs.is_empty()); } - #[test] + #[rstest::rstest] + fn test_status_information_read_card() { + let bytes = get_bytes("status_information_read_card.blob"); + let expected = StatusInformation { + result_code: Some(0), + track_2_data: Some("6725904411001000142d24122012386013860f".to_string()), + tlv: Some(tlv::StatusInformation { + maximum_pre_autorisation: Some(10000), + card_identification_item: Some("3f56a32065cc4dbe8330c37609f91996".to_string()), + uuid: Some("00000000000008b3c880".to_string()), + ats: Some("0c788074038031c073d631c0".to_string()), + card_type: Some(1), + sub_type: Some("fe04".to_string()), + atqa: Some("0400".to_string()), + sak: Some(32), + subs: Vec::new(), + subs_on_card: Some(tlv::SubsOnCard { + subs: vec![ + tlv::Subs { + application_id: Some("a0000003591010028001".to_string()), + card_type: Some("0005".to_string()), + }, + tlv::Subs { + application_id: Some("a0000000043060".to_string()), + card_type: Some("002e".to_string()), + }, + ], + }), + }), + ..StatusInformation::default() + }; + assert_eq!( + expected, + StatusInformation::zvt_deserialize(&bytes).unwrap().0 + ); + } + + #[rstest::rstest] fn test_receipt_printout_completion() { let bytes = get_bytes("1680728219.054216000_pt_ecr.blob"); let expected = ReceiptPrintoutCompletion { @@ -626,7 +680,7 @@ pub mod tests { ); } - #[test] + #[rstest::rstest] fn test_pre_auth_data() { let bytes = get_bytes("1680728162.033575000_ecr_pt.blob"); let expected = Reservation { @@ -644,7 +698,7 @@ pub mod tests { assert_eq!(Reservation::zvt_deserialize(&bytes).unwrap().0, expected,); } - #[test] + #[rstest::rstest] fn test_registration() { let bytes = get_bytes("1681273860.511128000_ecr_pt.blob"); let expected = Registration { @@ -657,7 +711,7 @@ pub mod tests { assert_eq!(Registration::zvt_deserialize(&bytes).unwrap().0, expected); } - #[test] + #[rstest::rstest] fn test_pre_auth_reversal() { let bytes = get_bytes("1680728213.562478000_ecr_pt.blob"); let expected = PreAuthReversal { @@ -671,7 +725,7 @@ pub mod tests { ); } - #[test] + #[rstest::rstest] fn test_completion_data() { let bytes = get_bytes("1680761818.641601000_pt_ecr.blob"); let golden = CompletionData { @@ -685,7 +739,7 @@ pub mod tests { assert_eq!(CompletionData::zvt_deserialize(&bytes).unwrap().0, golden); } - #[test] + #[rstest::rstest] fn test_end_of_day() { let bytes = get_bytes("1681282621.302434000_ecr_pt.blob"); let expected = EndOfDay { password: 123456 }; @@ -694,7 +748,7 @@ pub mod tests { assert_eq!(bytes, expected.zvt_serialize()); } - #[test] + #[rstest::rstest] fn test_partial_reversal() { let bytes = get_bytes("1681455683.221609000_ecr_pt.blob"); let expected = PartialReversal { @@ -715,7 +769,7 @@ pub mod tests { ); } - #[test] + #[rstest::rstest] fn test_intermediate_status() { let bytes = get_bytes("1680728162.647465000_pt_ecr.blob"); let expected = IntermediateStatusInformation { @@ -731,7 +785,7 @@ pub mod tests { assert_eq!(bytes, expected.zvt_serialize()); } - #[test] + #[rstest::rstest] fn test_print_text_block() { let bytes = get_bytes("1680728215.585561000_pt_ecr.blob"); let actual = PrintTextBlock::zvt_deserialize(&bytes).unwrap().0; @@ -743,7 +797,7 @@ pub mod tests { assert_eq!(bytes, actual.zvt_serialize()); } - #[test] + #[rstest::rstest] fn test_text_block_system_information() { let bytes = get_bytes("print_system_configuration_reply.blob"); let actual = PrintTextBlock::zvt_deserialize(&bytes).unwrap().0; @@ -755,7 +809,7 @@ pub mod tests { assert_eq!(bytes, actual.zvt_serialize()); } - #[test] + #[rstest::rstest] fn test_partial_reversal_abort() { let bytes = get_bytes("partial_reversal.blob"); let actual = PartialReversalAbort::zvt_deserialize(&bytes).unwrap().0; diff --git a/zvt/src/packets/tlv.rs b/zvt/src/packets/tlv.rs index 549722b..512f910 100644 --- a/zvt/src/packets/tlv.rs +++ b/zvt/src/packets/tlv.rs @@ -1,17 +1,32 @@ -use crate::{encoding, length, Zvt}; +use crate::{encoding, Zvt}; use chrono::NaiveDateTime; #[derive(Debug, Default, PartialEq, Zvt)] pub struct Subs { - #[zvt_bmp(number = 0x43, length = length::Tlv, encoding = encoding::Hex)] + #[zvt_tlv(tag = 0x41, encoding = encoding::Hex)] + pub card_type: Option, + + #[zvt_tlv(tag = 0x43, encoding = encoding::Hex)] pub application_id: Option, } +#[derive(Debug, Default, PartialEq, Zvt)] +pub struct SubsOnCard { + #[zvt_tlv(tag = 0x60)] + pub subs: Vec, +} + #[derive(Debug, Default, PartialEq, Zvt)] pub struct StatusInformation { #[zvt_tlv(tag = 0x4c, encoding = encoding::Hex)] pub uuid: Option, + #[zvt_tlv(tag = 0x1f0b, encoding = encoding::Bcd)] + pub maximum_pre_autorisation: Option, + + #[zvt_tlv(tag = 0x1f14, encoding = encoding::Hex)] + pub card_identification_item: Option, + #[zvt_tlv(tag = 0x1f45, encoding = encoding::Hex)] pub ats: Option, @@ -31,6 +46,9 @@ pub struct StatusInformation { // this is a vector. #[zvt_tlv(tag = 0x60)] pub subs: Vec, + + #[zvt_tlv(tag = 0x62)] + pub subs_on_card: Option, } #[derive(Debug, Default, PartialEq, Zvt)] @@ -41,16 +59,16 @@ pub struct StatusEnquiry { #[derive(Debug, PartialEq, Zvt)] pub struct DeviceInformation { - #[zvt_bmp(number = 0x1f40, length = length::Tlv)] + #[zvt_tlv(tag = 0x1f40)] pub device_name: Option, - #[zvt_bmp(number = 0x1f41, length = length::Tlv)] + #[zvt_tlv(tag = 0x1f41)] pub software_version: Option, - #[zvt_bmp(number = 0x1f42, length = length::Tlv, encoding = encoding::Bcd)] + #[zvt_tlv(tag = 0x1f42, encoding = encoding::Bcd)] pub serial_number: Option, - #[zvt_bmp(number = 0x1f43, length = length::Tlv)] + #[zvt_tlv(tag = 0x1f43)] pub device_state: Option, } @@ -109,7 +127,7 @@ pub struct ReadCard { #[derive(Debug, PartialEq, Zvt)] pub struct ZvtString { - #[zvt_bmp(number = 0x07, length=length::Tlv)] + #[zvt_tlv(tag = 0x07)] pub line: String, } diff --git a/zvt/tests/derive.rs b/zvt/tests/derive.rs index 8e64fb6..13a55ca 100644 --- a/zvt/tests/derive.rs +++ b/zvt/tests/derive.rs @@ -194,7 +194,7 @@ fn test_nested() { #[derive(Zvt, PartialEq, Debug)] struct Outer { a: u16, - #[zvt_bmp(number = 0x12, length = length::Tlv)] + #[zvt_tlv(tag = 0x12)] b: Option, } diff --git a/zvt_builder/src/length.rs b/zvt_builder/src/length.rs index 62eb223..b54daa0 100644 --- a/zvt_builder/src/length.rs +++ b/zvt_builder/src/length.rs @@ -94,7 +94,7 @@ impl Length for LlvImpl { let mut k = input; let mut rv = vec![0; N]; for i in (0..N).rev() { - rv[i] = (k % 10) as u8; + rv[i] = 0xf0 | (k % 10) as u8; k /= 10; } rv diff --git a/zvt_cli/src/main.rs b/zvt_cli/src/main.rs index c41d988..d6f630d 100644 --- a/zvt_cli/src/main.rs +++ b/zvt_cli/src/main.rs @@ -143,6 +143,10 @@ struct ReservationArgs { #[argh(option, default = "64")] payment_type: u8, + /// track 2 data to identify past read card. + #[argh(option)] + track_2_data: Option, + /// bmp_prefix. If this is set, bmp_data must be set too. #[argh(option)] bmp_prefix: Option, @@ -447,12 +451,14 @@ fn prep_bmp_data( _ => bail!("Either none or both of bmp_data and bmp_prefix must be given."), } } + async fn reservation(socket: &mut PacketTransport, args: ReservationArgs) -> Result<()> { let tlv = prep_bmp_data(args.bmp_prefix, args.bmp_data)?; let request = packets::Reservation { currency: Some(args.currency_code), amount: Some(args.amount), payment_type: Some(args.payment_type), + track_2_data: args.track_2_data, tlv, ..packets::Reservation::default() }; diff --git a/zvt_derive/src/lib.rs b/zvt_derive/src/lib.rs index 361270d..eb3c2d7 100644 --- a/zvt_derive/src/lib.rs +++ b/zvt_derive/src/lib.rs @@ -274,11 +274,18 @@ fn derive_deserialize( Err(_) => break, Ok(data) => data.0, }; + log::debug!("Found tag: 0x{:X}", tag.0); // Try to match our tags match tag.0 { #(#opt_field_quotes)* - _ => {break;} + _ => { + // TODO(hrapp): This should return Error::WrongTag, however since this is + // highly backwards incompatible and we see this warning quite a bit in + // (uncritical) packages in prod, we did not do this yet. + log::error!("Unhandled tag: 0x{:X}. We give up parsing here, your data is only partially interpreted.", tag.0); + break; + } } }