mirror of
https://git.eta.st/eta/rsp6-decoder.git
synced 2024-11-24 18:45:40 +00:00
complete decoding
This commit is contained in:
parent
62e17a96f5
commit
b4e8424fa1
67
Cargo.lock
generated
67
Cargo.lock
generated
@ -20,6 +20,18 @@ version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
@ -69,6 +81,12 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.6"
|
||||
@ -229,6 +247,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@ -285,11 +309,13 @@ name = "rsp6-decoder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitvec",
|
||||
"hex",
|
||||
"rand",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -378,6 +404,38 @@ dependencies = [
|
||||
"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]]
|
||||
name = "typenum"
|
||||
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"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.5.7"
|
||||
|
@ -11,4 +11,6 @@ anyhow = "1.0"
|
||||
serde_json = "1.0"
|
||||
rsa = "0.7"
|
||||
rand = "0.8"
|
||||
bitvec = "1.0"
|
||||
hex = { version = "0.4", features = ["serde"] }
|
||||
time = { version = "0.3", features = ["macros", "serde"] }
|
@ -1,10 +1,10 @@
|
||||
pub fn m296a(a: &[u8], i: usize) -> bool {
|
||||
(a[i / 8] >> (7 - (i * 8))) == 1
|
||||
pub fn slice_bool(a: &[u8], index: usize) -> bool {
|
||||
(a[index / 8] >> (7 - (index % 8))) == 1
|
||||
}
|
||||
|
||||
pub fn m295a(a: &[u8], i: usize, i2: usize) -> u32 {
|
||||
let i = i as u32;
|
||||
let i2 = i2 as u32;
|
||||
pub fn slice_int(a: &[u8], start_bit: usize, length_bits: usize) -> u32 {
|
||||
let i = start_bit as u32;
|
||||
let i2 = length_bits as u32;
|
||||
let mut i3: u32 = (i as u32) & 7;
|
||||
let mut i4: u32 = i3;
|
||||
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)
|
||||
}
|
||||
|
||||
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 i = i as u32;
|
||||
let mut i2 = i2 as u32;
|
||||
let mut i = start_bit as u32;
|
||||
let mut i2 = chars as u32;
|
||||
let mut i3: u32 = i & 7;
|
||||
let mut i4: u32 = i3;
|
||||
let mut i5: u32 = i >> 3;
|
||||
|
28
src/main.rs
28
src/main.rs
@ -1,10 +1,18 @@
|
||||
use crate::cursed::{slice_base64, slice_bool, slice_int};
|
||||
use crate::keys::IssuerKeyStore;
|
||||
use crate::payload::{CapitalismDateTime, Rsp6Ticket};
|
||||
use anyhow::anyhow;
|
||||
use bitvec::field::BitField;
|
||||
use bitvec::order::Msb0;
|
||||
use bitvec::view::BitView;
|
||||
use rsa::BigUint;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
mod cursed;
|
||||
mod keys;
|
||||
mod payload;
|
||||
|
||||
fn base26_decode(input: &str) -> BigUint {
|
||||
let mut out = BigUint::new(Vec::new());
|
||||
@ -56,8 +64,11 @@ fn strip_padding(tkt: &[u8]) -> Option<&[u8]> {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let iks = IssuerKeyStore::new();
|
||||
println!("[+] Loaded {} public keys!", iks.keys.len());
|
||||
println!("[+] Reading ticket.dat...");
|
||||
let ticket_str = fs::read_to_string("./ticket.dat")?;
|
||||
println!("[+] mmm, give me a tasty ticket on stdin please");
|
||||
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 {
|
||||
return Err(anyhow!("ticket too short"));
|
||||
}
|
||||
@ -83,19 +94,20 @@ fn main() -> anyhow::Result<()> {
|
||||
let message = ticket.modpow(&exponent, &modulus).to_bytes_be();
|
||||
if let Some(unpadded) = strip_padding(&message) {
|
||||
println!("done! {:?}", unpadded);
|
||||
let ticket_ref_inner = cursed::m297a(unpadded, 8, 9);
|
||||
let extra_bit = cursed::m297a(unpadded, 62, 1);
|
||||
let ticket_ref_inner = cursed::slice_base64(unpadded, 8, 9);
|
||||
let extra_bit = cursed::slice_base64(unpadded, 62, 1);
|
||||
let inner_data = format!("{}{}", ticket_ref_inner, extra_bit);
|
||||
let outer_data = &ticket_str[2..12];
|
||||
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: {:#?}", Rsp6Ticket::decode(unpadded));
|
||||
return Ok(());
|
||||
}
|
||||
/*else {
|
||||
} else {
|
||||
println!("failed decrypt: {:?}", message);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
Err(anyhow!("no valid decryptions"))
|
||||
}
|
||||
|
262
src/payload.rs
Normal file
262
src/payload.rs
Normal 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 _)
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
06DNQL8KK5H00TTRANEBZCYPNQVMMYJBOJBONYSIYXTREYFSHTZFZEXWTVBNXJBFVOFBMXVQPZTFWVYSWYKINRXRVDCCUWUERKQZKYBPVIIAPJOOFJJXUBFGNVXGXTCFPBHXYVPEKWIURBEOYTYNZUXWVIXHAODACOQLZEQKRUNGWSJHIIWOYSNXJKVYWIGLWCIZKAHFKKAKRDUQSQBGEJMOFCSHSKXSFDDKYCFQI
|
Loading…
Reference in New Issue
Block a user