complete decoding

This commit is contained in:
eta 2022-12-30 13:33:34 +00:00
parent 62e17a96f5
commit b4e8424fa1
6 changed files with 360 additions and 18 deletions

67
Cargo.lock generated
View File

@ -20,6 +20,18 @@ version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -69,6 +81,12 @@ dependencies = [
"crypto-common", "crypto-common",
] ]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.6" version = "0.14.6"
@ -229,6 +247,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -285,11 +309,13 @@ name = "rsp6-decoder"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitvec",
"hex", "hex",
"rand", "rand",
"rsa", "rsa",
"serde", "serde",
"serde_json", "serde_json",
"time",
] ]
[[package]] [[package]]
@ -378,6 +404,38 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "time"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [
"time-core",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.16.0" version = "1.16.0"
@ -402,6 +460,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.5.7" version = "1.5.7"

View File

@ -11,4 +11,6 @@ anyhow = "1.0"
serde_json = "1.0" serde_json = "1.0"
rsa = "0.7" rsa = "0.7"
rand = "0.8" rand = "0.8"
bitvec = "1.0"
hex = { version = "0.4", features = ["serde"] } hex = { version = "0.4", features = ["serde"] }
time = { version = "0.3", features = ["macros", "serde"] }

View File

@ -1,10 +1,10 @@
pub fn m296a(a: &[u8], i: usize) -> bool { pub fn slice_bool(a: &[u8], index: usize) -> bool {
(a[i / 8] >> (7 - (i * 8))) == 1 (a[index / 8] >> (7 - (index % 8))) == 1
} }
pub fn m295a(a: &[u8], i: usize, i2: usize) -> u32 { pub fn slice_int(a: &[u8], start_bit: usize, length_bits: usize) -> u32 {
let i = i as u32; let i = start_bit as u32;
let i2 = i2 as u32; let i2 = length_bits as u32;
let mut i3: u32 = (i as u32) & 7; let mut i3: u32 = (i as u32) & 7;
let mut i4: u32 = i3; let mut i4: u32 = i3;
let mut i5: u32 = (i as u32) >> 3; let mut i5: u32 = (i as u32) >> 3;
@ -22,10 +22,10 @@ pub fn m295a(a: &[u8], i: usize, i2: usize) -> u32 {
i8 >> (32 - i2) i8 >> (32 - i2)
} }
pub fn m297a(a: &[u8], i: usize, i2: usize) -> String { pub fn slice_base64(a: &[u8], start_bit: usize, chars: usize) -> String {
let mut sb = String::new(); let mut sb = String::new();
let mut i = i as u32; let mut i = start_bit as u32;
let mut i2 = i2 as u32; let mut i2 = chars as u32;
let mut i3: u32 = i & 7; let mut i3: u32 = i & 7;
let mut i4: u32 = i3; let mut i4: u32 = i3;
let mut i5: u32 = i >> 3; let mut i5: u32 = i >> 3;

View File

@ -1,10 +1,18 @@
use crate::cursed::{slice_base64, slice_bool, slice_int};
use crate::keys::IssuerKeyStore; use crate::keys::IssuerKeyStore;
use crate::payload::{CapitalismDateTime, Rsp6Ticket};
use anyhow::anyhow; use anyhow::anyhow;
use bitvec::field::BitField;
use bitvec::order::Msb0;
use bitvec::view::BitView;
use rsa::BigUint; use rsa::BigUint;
use std::fs; use std::fs;
use std::io::Read;
use time::PrimitiveDateTime;
mod cursed; mod cursed;
mod keys; mod keys;
mod payload;
fn base26_decode(input: &str) -> BigUint { fn base26_decode(input: &str) -> BigUint {
let mut out = BigUint::new(Vec::new()); let mut out = BigUint::new(Vec::new());
@ -56,8 +64,11 @@ fn strip_padding(tkt: &[u8]) -> Option<&[u8]> {
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let iks = IssuerKeyStore::new(); let iks = IssuerKeyStore::new();
println!("[+] Loaded {} public keys!", iks.keys.len()); println!("[+] Loaded {} public keys!", iks.keys.len());
println!("[+] Reading ticket.dat..."); println!("[+] mmm, give me a tasty ticket on stdin please");
let ticket_str = fs::read_to_string("./ticket.dat")?; let ticket_str = std::io::stdin()
.lines()
.next()
.ok_or_else(|| anyhow!("that was a bit rude, give me an actual ticket"))??;
if ticket_str.len() < 16 { if ticket_str.len() < 16 {
return Err(anyhow!("ticket too short")); return Err(anyhow!("ticket too short"));
} }
@ -83,19 +94,20 @@ fn main() -> anyhow::Result<()> {
let message = ticket.modpow(&exponent, &modulus).to_bytes_be(); let message = ticket.modpow(&exponent, &modulus).to_bytes_be();
if let Some(unpadded) = strip_padding(&message) { if let Some(unpadded) = strip_padding(&message) {
println!("done! {:?}", unpadded); println!("done! {:?}", unpadded);
let ticket_ref_inner = cursed::m297a(unpadded, 8, 9); let ticket_ref_inner = cursed::slice_base64(unpadded, 8, 9);
let extra_bit = cursed::m297a(unpadded, 62, 1); let extra_bit = cursed::slice_base64(unpadded, 62, 1);
let inner_data = format!("{}{}", ticket_ref_inner, extra_bit); let inner_data = format!("{}{}", ticket_ref_inner, extra_bit);
let outer_data = &ticket_str[2..12]; let outer_data = &ticket_str[2..12];
if inner_data != outer_data { if inner_data != outer_data {
return Err(anyhow!("failed to validate inner and outer data")); eprintln!("mismatch: {} vs {}", inner_data, outer_data);
// return Err(anyhow!("failed to validate inner and outer data"));
} }
println!("[+] ticket validated!"); println!("[+] ticket validated!");
println!("[+] ticket: {:#?}", Rsp6Ticket::decode(unpadded));
return Ok(()); return Ok(());
} } else {
/*else {
println!("failed decrypt: {:?}", message); println!("failed decrypt: {:?}", message);
}*/ }
} }
Err(anyhow!("no valid decryptions")) Err(anyhow!("no valid decryptions"))
} }

262
src/payload.rs Normal file
View File

@ -0,0 +1,262 @@
//! Decoding the actual inner decrypted payload bit.
use crate::{slice_base64, slice_bool};
use anyhow::anyhow;
use bitvec::field::BitField;
use bitvec::order::Msb0;
use bitvec::slice::BitSlice;
use bitvec::view::BitView;
use time::macros::datetime;
use time::{Date, Duration, PrimitiveDateTime, Time};
#[derive(Copy, Clone, Debug)]
pub enum CouponType {
Single = 0,
Season = 1,
ReturnOutbound = 2,
ReturnInbound = 3,
}
#[derive(Clone, Debug)]
pub struct TicketPurchaseDetails {
pub purchase_time: PrimitiveDateTime,
pub price_pence: u32,
pub purchase_reference: Option<String>,
pub days_of_validity: u16,
}
#[derive(Clone, Debug)]
pub struct Reservation {
pub retail_service_id: String,
pub coach: char,
pub seat_number: u8,
pub seat_letter: Option<char>,
}
impl Reservation {
pub fn decode(resv: &BitSlice<u8, Msb0>) -> anyhow::Result<Self> {
if resv.len() != 45 {
return Err(anyhow!("reservation length {}, not 45", resv.len()));
}
let rsid_1 = char::from(resv[0..6].load_be::<u8>() + 32);
let rsid_2 = char::from(resv[6..12].load_be::<u8>() + 32);
let rsid_nums: u16 = resv[12..26].load_be();
let retail_service_id = format!("{}{}{:04}", rsid_1, rsid_2, rsid_nums);
let coach = char::from(resv[26..32].load_be::<u8>() + 32);
let seat_letter = char::from(resv[32..38].load_be::<u8>() + 32);
let seat_letter = (seat_letter != ' ').then_some(seat_letter);
let seat_number: u8 = resv[38..45].load_be();
Ok(Self {
retail_service_id,
coach,
seat_number,
seat_letter,
})
}
}
#[derive(Clone, Debug)]
pub struct Rsp6Ticket {
pub manually_inspect: bool,
pub ticket_reference: String,
pub checksum: char,
pub version: u8,
pub standard_class: bool,
pub lennon_ticket_type: String,
pub fare: String,
pub origin_nlc: String,
pub destination_nlc: String,
pub retailer_id: String,
pub child_ticket: bool,
pub coupon_type: CouponType,
pub discount_code: u16,
pub route_code: u32,
pub start_date: Date,
pub depart_time: Option<Time>,
pub passenger_id: Option<String>,
pub passenger_name: Option<String>,
pub passenger_gender: Option<u8>,
pub restriction_code: Option<String>,
pub bidirectional: bool,
pub limited_duration: Option<Duration>,
pub purchase_details: Option<TicketPurchaseDetails>,
pub reservations: Vec<Reservation>,
pub free_text: Option<String>,
}
impl Rsp6Ticket {
fn base64(tkt: &[u8], from: usize, to: usize) -> String {
let chars = (to - from) / 6;
assert_eq!(chars * 6, to - from);
slice_base64(tkt, from, chars)
}
fn decode_limited_duration(dur: u8) -> Option<Duration> {
Some(match dur {
1 => Duration::minutes(15),
2 => Duration::minutes(30),
3 => Duration::minutes(45),
4 => Duration::hours(1),
5 => Duration::minutes(90),
6 => Duration::hours(2),
7 => Duration::hours(3),
8 => Duration::hours(4),
9 => Duration::hours(5),
10 => Duration::hours(6),
11 => Duration::hours(8),
12 => Duration::hours(10),
13 => Duration::hours(12),
14 => Duration::hours(18),
_ => return None,
})
}
fn decode_passenger_id(id: u32) -> Option<String> {
if id == 0 {
return None;
}
let prefix = match id / 10000 {
0 => return None,
1 => "CCD",
2 => "DCD",
3 => "PPT",
4 => "DLC",
5 => "AFC",
6 => "NIC",
7 => "NHS",
_ => "???",
};
Some(format!("{}{:04}", prefix, (id % 10000)))
}
pub fn decode(tkt: &[u8]) -> anyhow::Result<Self> {
let bit_tkt = tkt.view_bits::<Msb0>();
let manually_inspect = slice_bool(tkt, 0);
let ticket_reference = Self::base64(tkt, 8, 62);
let checksum = Self::base64(tkt, 62, 68).chars().next().unwrap();
let version: u8 = bit_tkt[68..72].load_be();
let standard_class = slice_bool(tkt, 72);
let lennon_ticket_type = Self::base64(tkt, 73, 91);
let fare = Self::base64(tkt, 91, 109);
let origin_nlc = Self::base64(tkt, 109, 133);
let destination_nlc = Self::base64(tkt, 133, 157);
let retailer_id = Self::base64(tkt, 157, 181);
let is_child = slice_bool(tkt, 181);
let coupon_type = match bit_tkt[182..184].load_be::<u8>() {
0 => CouponType::Single,
1 => CouponType::Season,
2 => CouponType::ReturnOutbound,
3 => CouponType::ReturnInbound,
_ => unreachable!(), // only 2-bit int
};
let discount_code: u16 = bit_tkt[184..194].load_be();
let route_code: u32 = bit_tkt[194..211].load_be();
let start_time_days: u32 = bit_tkt[211..225].load_be();
let start_time_secs: u32 = bit_tkt[225..236].load_be();
let start_time: PrimitiveDateTime =
CapitalismDateTime::new(start_time_days, start_time_secs).into();
let depart_time_flag: u8 = bit_tkt[236..238].load_be();
// FIXME
let depart_time = (depart_time_flag == 0).then_some(start_time.time());
let start_date = start_time.date();
let passenger_id = Self::decode_passenger_id(bit_tkt[238..255].load_be());
let passenger_name = Self::base64(tkt, 255, 327);
let passenger_name = (!passenger_name.trim().is_empty()).then_some(passenger_name);
let passenger_gender: u8 = bit_tkt[327..329].load_be();
let passenger_gender = (passenger_gender != 0).then_some(passenger_gender);
let restriction_code = Self::base64(tkt, 329, 347);
let restriction_code = (!restriction_code.trim().is_empty()).then_some(restriction_code);
let bidirectional = slice_bool(tkt, 372);
let limited_duration = Self::decode_limited_duration(bit_tkt[379..383].load_be());
let is_full_ticket = slice_bool(tkt, 384);
let purchase_details = if is_full_ticket {
let purchase_time_days: u32 = bit_tkt[390..404].load_be();
let purchase_time_secs: u32 = bit_tkt[404..415].load_be();
let purchase_time: PrimitiveDateTime =
CapitalismDateTime::new(purchase_time_days, purchase_time_secs).into();
let price_pence: u32 = bit_tkt[415..436].load_be();
let purchase_reference = Self::base64(tkt, 449, 497);
let purchase_reference =
(!purchase_reference.trim().is_empty()).then_some(purchase_reference);
let mut days_of_validity = bit_tkt[497..506].load_be();
if days_of_validity == 0 {
days_of_validity = 1;
}
Some(TicketPurchaseDetails {
purchase_time,
price_pence,
purchase_reference,
days_of_validity,
})
} else {
None
};
let reservations_start = if is_full_ticket { 512 } else { 390 };
let reservations_count: u8 = bit_tkt[386..390].load_be();
let reservations = (0..reservations_count)
.map(|x| {
let start = reservations_start + (45 * x) as usize;
let end = reservations_start + (45 * (1 + x)) as usize;
Reservation::decode(&bit_tkt[start..end])
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
manually_inspect,
ticket_reference,
checksum,
version,
standard_class,
lennon_ticket_type,
fare,
origin_nlc,
destination_nlc,
retailer_id,
child_ticket: is_child,
coupon_type,
discount_code,
route_code,
start_date,
depart_time,
passenger_id,
passenger_name,
passenger_gender,
restriction_code,
bidirectional,
limited_duration,
purchase_details,
reservations,
free_text: None,
})
}
}
#[derive(Copy, Clone, Debug)]
pub struct CapitalismDateTime {
days: u32,
minutes: u32,
}
impl CapitalismDateTime {
pub const PRIVATISATION_EPOCH: PrimitiveDateTime = datetime!(1997-01-01 00:00:00);
pub fn new(days: u32, minutes: u32) -> Self {
Self { days, minutes }
}
}
impl Into<PrimitiveDateTime> for CapitalismDateTime {
fn into(self) -> PrimitiveDateTime {
CapitalismDateTime::PRIVATISATION_EPOCH
+ Duration::days(self.days as _)
+ Duration::minutes(self.minutes as _)
}
}

View File

@ -1 +0,0 @@
06DNQL8KK5H00TTRANEBZCYPNQVMMYJBOJBONYSIYXTREYFSHTZFZEXWTVBNXJBFVOFBMXVQPZTFWVYSWYKINRXRVDCCUWUERKQZKYBPVIIAPJOOFJJXUBFGNVXGXTCFPBHXYVPEKWIURBEOYTYNZUXWVIXHAODACOQLZEQKRUNGWSJHIIWOYSNXJKVYWIGLWCIZKAHFKKAKRDUQSQBGEJMOFCSHSKXSFDDKYCFQI