This commit is contained in:
eta 2023-01-14 00:41:33 +00:00
parent 7a8648da9c
commit 4924b13893
19 changed files with 17868 additions and 96 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target
rsp6-webshite/dist
*.swp

View File

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/rsp6-webshite" vcs="Git" />
</component>
</project>

174
Cargo.lock generated
View File

@ -26,6 +26,34 @@ dependencies = [
"wyz",
]
[[package]]
name = "bumpalo"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen",
]
[[package]]
name = "funty"
version = "2.0.0"
@ -47,6 +75,42 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "js-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "num-bigint"
version = "0.4.3"
@ -77,6 +141,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "proc-macro2"
version = "1.0.49"
@ -107,11 +177,16 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bitvec",
"console_error_panic_hook",
"hex",
"lazy_static",
"num-bigint",
"serde",
"serde-wasm-bindgen",
"serde_json",
"time",
"wasm-bindgen",
"wee_alloc",
]
[[package]]
@ -129,6 +204,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
@ -201,6 +287,94 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "wasm-bindgen"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wyz"
version = "0.5.1"

View File

@ -5,6 +5,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[features]
wasm = ["wasm-bindgen", "lazy_static", "serde-wasm-bindgen", "wee_alloc", "console_error_panic_hook"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
@ -12,4 +18,9 @@ serde_json = "1.0"
num-bigint = "0.4"
bitvec = "1.0"
hex = { version = "0.4", features = ["serde"] }
time = { version = "0.3", features = ["macros", "serde", "serde-human-readable"] }
time = { version = "0.3", features = ["macros", "serde", "serde-human-readable"] }
wasm-bindgen = { version = "0.2", optional = true }
lazy_static = { version = "1.4", optional = true }
serde-wasm-bindgen = { version = "0.4", optional = true }
wee_alloc = { version = "0.4", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2023 eta
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

2
rsp6-webshite/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

5
rsp6-webshite/bootstrap.js vendored Normal file
View File

@ -0,0 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import("./index.js")
.catch(e => console.error("Error importing `index.js`:", e));

3447
rsp6-webshite/fares.json Normal file

File diff suppressed because it is too large Load Diff

587
rsp6-webshite/govuk.css Normal file

File diff suppressed because one or more lines are too long

160
rsp6-webshite/index.html Normal file
View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>RSP6 decoder</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<link rel="stylesheet" href="./govuk.css"/>
</head>
<body class="govuk-template__body">
<a href="#main-content" class="govuk-skip-link">Skip to main content</a>
<header class="govuk-header" role="banner">
<div class="govuk-header__container govuk-width-container" style="border-bottom: 10px solid #1d70b8;">
<div class="govuk-header__logo">
<a href="/" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype-text">tickets™<small> by eta</small></span>
</a>
</div>
</div>
</header>
<div class="govuk-width-container">
<main class="govuk-main-wrapper" id="main-content" role="main">
<h1 class="govuk-heading-l">Decode a National Rail mobile ticket</h1>
<noscript>
<div class="govuk-error-summary" data-module="govuk-error-summary" id="error-banner" style="display: none;">
<div role="alert">
<h2 class="govuk-error-summary__title" id="error-header">
JavaScript is required
</h2>
<div class="govuk-error-summary__body">
<p class="govuk-body" id="error-text">Sorry! You need JavaScript to run this page at all, since it does all the decoding inside your browser.</p>
</div>
</div>
</div>
</noscript>
<div class="govuk-error-summary" data-module="govuk-error-summary" id="error-banner" style="display: none;">
<div role="alert">
<h2 class="govuk-error-summary__title" id="error-header">
There is a problem
</h2>
<div class="govuk-error-summary__body">
<p class="govuk-body" id="error-text"></p>
</div>
</div>
</div>
<p class="govuk-body">Scan a National Rail mTicket with your device's camera, and we'll tell you what data is inside! (If it works, that is. This was thrown together in an evening and is probably quite unreliable.)</p>
<div class="govuk-inset-text">
Ticket decoding happens entirely inside your browser; no data is transmitted back to us.
</div>
<button class="govuk-button govuk-button--start" data-module="govuk-button" disabled id="scan-button">
Scan now
<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z" />
</svg>
</button>
<details class="govuk-details" id="screenshot-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Use a screenshot instead
</span>
</summary>
<div class="govuk-details__text">
<p class="govuk-body"><strong>Please note!</strong> You'll need to crop your screenshot so only the barcode bit is showing, otherwise it might fail to scan. I apologise for how finicky it is; you might need to play around with it.</p>
<div class="govuk-form-group">
<label class="govuk-label" for="file-upload-1">
Choose a file
</label>
<input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file">
<div id="image-sanctuary" style="display: none;"></div>
</div>
</div>
</details>
<details class="govuk-details" id="text-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
Use raw text instead
</span>
</summary>
<div class="govuk-details__text">
<p class="govuk-body">Does the barcode scanner just not want to work? Use another Aztec barcode scanner (there are plenty on the mobile app stores), and then copy the result into the bottom form.</p>
<div class="govuk-form-group" style="margin-bottom: 10px;">
<input class="govuk-input" id="raw-text-in" name="raw-text-in" type="text">
</div>
<button class="govuk-button" data-module="govuk-button" id="raw-text-btn">
Hope for the best
</button>
</div>
</details>
<div id="video-container" style="display: none;">
<video id="video" width="100%" height="300" style="border: 1px solid gray"></video>
</div>
<div id="ticket-content" style="display: none;">
<p class="govuk-body" id="ticket-fromto"></p>
<span class="govuk-heading-xl" style="font-weight: normal; text-align: center; margin-bottom: 0.5em !important;"><span id="origin">from</span><span id="dest">to</span></span>
<table class="govuk-table">
<tbody class="govuk-table__body" id="ticket-data">
</tbody>
</table>
<details class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
View ticket as JSON
</span>
</summary>
<div class="govuk-details__text">
<pre id="raw-json">
{
"lol": "lmao"
}
</pre>
</div>
</details>
</div>
<details class="govuk-details" id="raw-barcode-wrapper" style="display: none;">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text">
View scanned barcode
</span>
</summary>
<div class="govuk-details__text">
<pre id="raw-barcode">
{
"lol": "lmao"
}
</pre>
</div>
</details>
<div class="govuk-notification-banner" role="region" aria-labelledby="govuk-notification-banner-title" data-module="govuk-notification-banner" id="decode-banner" style="display: none;">
<div class="govuk-notification-banner__header">
<h2 class="govuk-notification-banner__title" id="govuk-notification-banner-title">
Decode failed
</h2>
</div>
<div class="govuk-notification-banner__content">
<p class="govuk-notification-banner__heading">
We couldn't understand that ticket
</p>
<p class="govuk-body">Try another one instead.</br><pre id="decode-reason"></span></p>
</div>
</div>
</main>
</div>
<footer class="govuk-footer" role="contentinfo">
<div class="govuk-width-container">
<div class="govuk-footer__meta">
<div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
<span class="govuk-footer__licence-description">
Powered by <a class="govuk-footer__link" href="https://git.eta.st/eta/rsp6-decoder">rsp6-decoder (Rust)</a>,
<a class="govuk-footer__link" href="https://github.com/zxing-js/library">zxing-js</a>, and
<a class="govuk-footer__link" href="https://github.com/rustwasm/wasm-bindgen">wasm-bindgen</a>.
Site design using the <a class="govuk-footer__link" href="https://github.com/alphagov/govuk-frontend">GOV.UK Design System</a>, under the terms of the <a class="govuk-footer__link" href="https://github.com/alphagov/govuk-frontend/blob/master/LICENSE.txt">MIT License</a>.
If you liked this, you might also enjoy <a class="govuk-footer__link" href="https://intertube.eta.st/">intertube</a>.
<span style="float: right;"><a class="govuk-footer__link" href="https://eta.st/">an eta project</a></span>
</span>
</div>
</div>
</div>
</footer>
<script src="./bootstrap.js"></script>
</body>
</html>

246
rsp6-webshite/index.js Normal file
View File

@ -0,0 +1,246 @@
console.log("[+] Wow, JavaScript!");
import * as wasm from "rsp6-decoder";
import { BrowserAztecCodeReader } from '@zxing/library';
let stations = require("./stations.json");
let fares = require("./fares.json");
window.stations = stations;
window.fares = fares;
window.addEventListener('load', function () {
console.log("[+] initialising wasm");
window.wasm = wasm;
window.wasm.init();
let video_div = document.getElementById("video-container");
let scan_button = document.getElementById("scan-button");
let error_banner = document.getElementById("error-banner");
let error_header = document.getElementById("error-header");
let error_text = document.getElementById("error-text");
let decode_banner = document.getElementById("decode-banner");
let decode_reason = document.getElementById("decode-reason");
let raw_barcode = document.getElementById("raw-barcode");
let raw_barcode_wrapper = document.getElementById("raw-barcode-wrapper");
let raw_json = document.getElementById("raw-json");
let ticket_data = document.getElementById("ticket-data");
let ticket_content = document.getElementById("ticket-content");
let file_upload = document.getElementById("file-upload-1");
const codeReader = new BrowserAztecCodeReader();
console.log("[+] Looks like everything initialised fine!");
let selectedDeviceId;
function error(header, text) {
video_div.style.display = "none";
error_header.innerHTML = header;
error_text.innerHTML = text;
error_banner.style.display = "block";
error_banner.focus();
error_banner.scrollIntoView();
}
function decodeError(text) {
decode_banner.style.display = "block";
decode_reason.innerHTML = text;
decode_banner.scrollIntoView();
}
function row(k, v) {
let tr = document.createElement("tr");
tr.classList.add("govuk-table__row");
let th = document.createElement("th");
th.classList.add("govuk-table__header");
th.scope = "row";
let td = document.createElement("td");
td.classList.add("govuk-table__cell");
th.innerHTML = k;
td.innerHTML = v;
tr.appendChild(th);
tr.appendChild(td);
ticket_data.appendChild(tr);
}
function nlcify(nlc) {
let data = stations[nlc];
if (!data || !data.desc) {
return nlc;
}
else {
if (data.crs) {
return data.crs;
}
else {
return nlc;
}
}
}
function handleTicket(ticket) {
window.ticket = ticket;
video_div.style.display = "none";
codeReader.reset();
console.log("Got ticket: " + ticket);
raw_barcode.innerHTML = ticket;
raw_barcode_wrapper.style.display = "block";
try {
let result = window.wasm.decode_ticket(window.ticket);
if (result["Err"]) {
decodeError(result["Err"]);
}
else {
let data = result["Ok"];
raw_json.innerHTML = JSON.stringify(data, null, 2);
ticket_data.innerHTML = "";
document.getElementById("origin").innerHTML = nlcify(data.origin_nlc);
document.getElementById("dest").innerHTML = nlcify(data.destination_nlc);
let fare = fares[data.fare] || ("Unknown fare (" + data.fare + ")");
let origin = stations[data.origin_nlc] ? stations[data.origin_nlc].desc : "NLC " + data.origin_nlc;
let destination = stations[data.destination_nlc] ? stations[data.destination_nlc].desc : "NLC " + data.destination_nlc;
let type = "";
if (data.coupon_type == "Single") {
type = "Single";
}
else if (data.coupon_type == "ReturnOutbound") {
type = "Return (outbound)";
}
else if (data.coupon_type == "ReturnInbound") {
type = "Return (inbound)";
}
else {
type = "<b>Season</b>";
}
document.getElementById("ticket-fromto").innerHTML = origin + " to " + destination;
row("Fare", fare);
row("Ticket type", type);
row("Start date", data.start_date);
if (data.depart_time_flag || data.depart_time != "00:00:00.0") {
let extra = "";
if (data.depart_time_flag == "Suggested") {
extra = " (suggested)";
}
else if (data.depart_time_flag == "Specific") {
extra = " (mandatory)";
}
row("Depart at", data.depart_time.substring(0, 5) + extra);
}
row("Ticket reference", data.issuer_id + data.ticket_reference);
if (data.passenger_name) {
row("Passenger name", data.passenger_name);
}
if (data.purchase_details) {
let dov = data.purchase_details.days_of_validity;
let maybe_s = "";
if (dov > 1) {
maybe_s = "s";
}
row("Valid for", dov + " day" + maybe_s);
}
row("Fare code", data.fare);
if (data.purchase_details) {
let price = data.purchase_details.price_pence;
row("Price", "£" + Math.floor(price / 100) + "." + (price % 100).toString().padStart(2, "0"));
row("Purchased", data.purchase_details.purchase_time.substring(0, 16));
if (data.purchase_details.purchase_reference) {
row("Purchase reference", data.purchase_details.purchase_reference);
}
}
let retailer = stations[data.retailer_id] ? stations[data.retailer_id].desc : "???";
row("Sold by", retailer + " (" + data.retailer_id + ")");
if (data.route_code != 0) {
row("Route code", data.route_code);
}
if (data.restriction_code) {
row("Restriction", data.restriction_code);
}
if (data.discount_code != 0) {
row("Discount code", data.route_code);
}
if (data.passenger_id) {
row("Passenger ID", data.passenger_id);
}
row("Lennon code", data.lennon_ticket_type);
row("Sequence number", data.sub_utn);
ticket_content.style.display = "block";
ticket_content.scrollIntoView();
}
}
catch (e) {
decodeError("exception: " + e);
}
}
window.handleTicket = handleTicket;
codeReader.getVideoInputDevices()
.then((videoInputDevices) => {
console.log("[+] Detected " + videoInputDevices.length + " video sources");
if (videoInputDevices.length >= 1) {
selectedDeviceId = videoInputDevices[0].deviceId;
scan_button.removeAttribute("disabled");
}
})
.catch((err) => {
error("Scanner failed", "Couldn't figure out what cameras you have on your device.<br/><pre>" + err + "</pre>");
});
document.getElementById('scan-button').addEventListener('click', () => {
decode_banner.style.display = "none";
ticket_content.style.display = "none";
codeReader.decodeFromInputVideoDevice(undefined, 'video').then((result) => {
error_banner.style.display = "none";
handleTicket(result);
}).catch((err) => {
error("Scanner failed", "Couldn't start the camera.<br/><pre>" + err + "</pre>");
console.error(err);
});
video_div.style.display = "block";
console.log(`[+] Barcode scanner started on device ${selectedDeviceId}`);
});
document.getElementById('video').addEventListener('play', () => {
console.log('[+] Video started playing');
video_div.scrollIntoView();
});
document.getElementById('raw-text-btn').addEventListener('click', () => {
console.log("[+] Raw text input pressed");
decode_banner.style.display = "none";
ticket_content.style.display = "none";
let value = document.getElementById('raw-text-in').value;
document.getElementById('text-details').removeAttribute('open');
handleTicket(value);
});
file_upload.addEventListener('change', () => {
if (file_upload.files.length > 0) {
console.log('[+] File selected');
const file = file_upload.files[0];
const uri = window.URL.createObjectURL(file);
console.log('[+] Scanning URL ' + uri);
const img = document.createElement("img");
img.src = uri;
img.videoWidth = 0;
let done = false;
document.getElementById('image-sanctuary').innerHTML = '';
document.getElementById('image-sanctuary').appendChild(img);
decode_banner.style.display = "none";
ticket_content.style.display = "none";
codeReader.reset();
setTimeout(() => {
if (!done) {
console.log("[+] Timed out!");
codeReader.reset();
}
}, 750);
codeReader.decodeFromImage(img).then((result) => {
console.log("[+] Screenshot decode done!");
done = true;
document.getElementById('screenshot-details').removeAttribute('open');
error_banner.style.display = "none";
window.URL.revokeObjectURL(uri);
handleTicket(result);
}).catch((err) => {
done = true;
console.error(err);
error("Barcode reader failed", "Couldn't detect a barcode in that screenshot. Try another image.<br/><pre>" + err + "</pre>");
window.URL.revokeObjectURL(uri);
});
}
});
})

12998
rsp6-webshite/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "rsp6-webshite",
"version": "0.1.0",
"description": "massive excess of web stuff to make tickets work",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
},
"author": "eta",
"license": "MIT",
"devDependencies": {
"copy-webpack-plugin": "^5.0.0",
"rsp6-decoder": "file:../pkg",
"webpack": "^4.29.3",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
},
"dependencies": {
"@zxing/library": "^0.19.1"
}
}

View File

@ -0,0 +1,23 @@
(defun f (x)
(let ((tr (string-right-trim '(#\Space #\Tab) x)))
(when (> (length tr) 0)
tr)))
(with-open-file (f "~/Downloads/CORPUSExtract.json")
(let* ((cl-json:*json-identifier-name-to-lisp* #'identity)
(data (cdr (assoc "TIPLOCDATA" (cl-json:decode-json f)
:test #'string=)))
(ret (make-hash-table :test 'equal)))
(loop
for entry in data
do (let ((nlc (cdr (assoc "NLC" entry :test #'string=)))
(crs (f (cdr (assoc "3ALPHA" entry :test #'string=))))
(desc (f (cdr (assoc "NLCDESC" entry :test #'string=)))))
(when (and nlc desc
(eql (rem nlc 100) 0))
(let ((nlc (format nil "~4,'0D" (/ nlc 100))))
(format t "~A → ~A / ~A~%" nlc crs desc)
(setf (gethash nlc ret)
`(("crs" . ,crs)
("desc" . ,desc)))))))
ret))

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path');
module.exports = {
entry: "./bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
mode: "development",
plugins: [
new CopyWebpackPlugin(['index.html', 'govuk.css'])
],
};

96
src/lib.rs Normal file
View File

@ -0,0 +1,96 @@
use crate::keys::IssuerKeyStore;
use crate::payload::Rsp6Ticket;
use anyhow::anyhow;
use num_bigint::BigUint;
pub mod keys;
pub mod payload;
#[cfg(feature = "wasm")]
pub mod wasm;
fn base26_decode(input: &str) -> BigUint {
let mut out = BigUint::new(Vec::new());
for val in input.as_bytes().iter().rev() {
out *= 26u32;
out += *val - b'A';
}
BigUint::from_bytes_le(&out.to_bytes_be())
}
fn strip_padding(tkt: &[u8]) -> Option<&[u8]> {
if tkt.is_empty() {
return None;
}
match tkt[0] {
1 => {
// PKCS#1 v1
let tkt = &tkt[1..];
let mut iter = tkt.iter();
loop {
match iter.next()? {
0 => {
return Some(iter.as_slice());
}
255 => {}
_ => return None,
}
}
}
2 => {
// PKCS#1 v2
let tkt = &tkt[1..];
let mut iter = tkt.iter();
loop {
match iter.next()? {
0 => {
return Some(iter.as_slice());
}
_ => {}
}
}
}
_ => None,
}
}
pub fn decode_ticket(iks: &IssuerKeyStore, ticket_str: &str) -> anyhow::Result<Rsp6Ticket> {
if ticket_str.len() < 16 {
return Err(anyhow!("ticket too short"));
}
if &ticket_str[0..2] != "06" {
return Err(anyhow!(
"ticket isn't a RSP6 ticket: magic was {}",
&ticket_str[0..2]
));
}
let issuer_id = &ticket_str[13..15];
let ticket = base26_decode(&ticket_str[15..]);
let keys = iks
.keys
.get(issuer_id)
.ok_or_else(|| anyhow!("unknown issuer ID {}", issuer_id))?;
for key in keys {
let message = ticket.modpow(&key.public_exponent, &key.modulus);
let message = message.to_bytes_be();
if let Some(unpadded) = strip_padding(&message) {
let ticket = Rsp6Ticket::decode(unpadded, issuer_id.into(), ticket_str[11..13].into())?;
return Ok(ticket);
}
}
Err(anyhow!(
"all signature unwrap attempts failed (tried {} keys for issuer {})",
keys.len(),
issuer_id
))
}
#[cfg(test)]
mod test {
#[test]
fn test_base26() {
assert_eq!(
super::base26_decode("RANEBZCYPNQVMMYJBOJBONYSIYXTREYFSHTZFZEXWTVBNXJBFVOFBMXVQPZTFWVYSWYKINRXRVDCCUWUERKQZKYBPVIIAPJOOFJJXUBFGNVXGXTCFPBHXYVPEKWIURBEOYTYNZUXWVIXHAODACOQLZEQKRUNGWSJHIIWOYSNXJKVYWIGLWCIZKAHFKKAKRDUQSQBGEJMOFCSHSKXSFDDKYCFQI").to_bytes_be(),
[53, 242, 184, 141, 14, 99, 169, 215, 200, 223, 85, 250, 45, 253, 184, 100, 225, 124, 82, 70, 138, 222, 246, 185, 192, 129, 247, 218, 24, 26, 249, 112, 74, 225, 71, 139, 27, 50, 218, 11, 93, 238, 232, 163, 151, 68, 159, 146, 80, 133, 11, 45, 57, 245, 163, 117, 218, 11, 187, 246, 18, 147, 88, 171, 133, 216, 166, 47, 232, 246, 198, 170, 99, 36, 120, 114, 73, 207, 19, 218, 202, 146, 158, 223, 107, 234, 171, 172, 20, 189, 133, 246, 192, 248, 57, 111, 65, 65, 135, 64, 241, 99, 87, 107, 75, 40, 224, 223, 100, 53, 180, 212, 53, 200, 172, 117, 127, 248, 193, 0, 147, 167, 222, 81, 135, 158, 135, 137]
)
}
}

View File

@ -1,103 +1,21 @@
use crate::keys::IssuerKeyStore;
use crate::payload::Rsp6Ticket;
use anyhow::anyhow;
use num_bigint::BigUint;
use rsp6_decoder::decode_ticket;
use rsp6_decoder::keys::IssuerKeyStore;
mod keys;
mod payload;
fn base26_decode(input: &str) -> BigUint {
let mut out = BigUint::new(Vec::new());
for val in input.as_bytes().iter().rev() {
out *= 26u32;
out += *val - b'A';
}
BigUint::from_bytes_le(&out.to_bytes_be())
}
fn strip_padding(tkt: &[u8]) -> Option<&[u8]> {
if tkt.is_empty() {
return None;
}
match tkt[0] {
1 => {
// PKCS#1 v1
let tkt = &tkt[1..];
let mut iter = tkt.iter();
loop {
match iter.next()? {
0 => {
return Some(iter.as_slice());
}
255 => {}
_ => return None,
}
}
}
2 => {
// PKCS#1 v2
let tkt = &tkt[1..];
let mut iter = tkt.iter();
loop {
match iter.next()? {
0 => {
return Some(iter.as_slice());
}
_ => {}
}
}
}
_ => None,
}
}
fn main() -> anyhow::Result<()> {
fn main() {
let iks = IssuerKeyStore::new();
eprintln!("[+] Loaded {} public keys!", iks.keys.len());
eprintln!("[+] mmm, give me a tasty ticket on stdin please");
eprintln!("Feed in a ticket on stdin (raw scan result starting 06...)");
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"));
}
if &ticket_str[0..2] != "06" {
return Err(anyhow!(
"ticket isn't a RSP6 ticket: magic was {}",
&ticket_str[0..2]
));
}
let issuer_id = &ticket_str[13..15];
let ticket_reference = format!("{}{}", issuer_id, &ticket_str[2..11]);
eprintln!("[+] RSP6 ticket, reference {}", ticket_reference);
let ticket = base26_decode(&ticket_str[15..]);
let keys = iks
.keys
.get(issuer_id)
.ok_or_else(|| anyhow!("unknown issuer ID {}", issuer_id))?;
for key in keys {
let message = ticket.modpow(&key.public_exponent, &key.modulus);
let message = message.to_bytes_be();
if let Some(unpadded) = strip_padding(&message) {
eprintln!("[+] decrypt done!");
let ticket = Rsp6Ticket::decode(unpadded, issuer_id.into(), ticket_str[11..13].into())?;
serde_json::to_writer_pretty(std::io::stdout(), &ticket)?;
return Ok(());
} else {
eprintln!("[-] failed decrypt: {:?}", message);
.expect("no ticket provided")
.expect("error reading ticket");
match decode_ticket(&iks, &ticket_str) {
Ok(t) => {
eprintln!("Decode successful.");
serde_json::to_writer_pretty(std::io::stdout(), &t).expect("error serializing ticket");
}
Err(e) => {
eprintln!("Failed to decode ticket: {}", e);
}
}
Err(anyhow!("no valid decryptions"))
}
#[cfg(test)]
mod test {
#[test]
fn test_base26() {
assert_eq!(
super::base26_decode("RANEBZCYPNQVMMYJBOJBONYSIYXTREYFSHTZFZEXWTVBNXJBFVOFBMXVQPZTFWVYSWYKINRXRVDCCUWUERKQZKYBPVIIAPJOOFJJXUBFGNVXGXTCFPBHXYVPEKWIURBEOYTYNZUXWVIXHAODACOQLZEQKRUNGWSJHIIWOYSNXJKVYWIGLWCIZKAHFKKAKRDUQSQBGEJMOFCSHSKXSFDDKYCFQI").to_bytes_be(),
[53, 242, 184, 141, 14, 99, 169, 215, 200, 223, 85, 250, 45, 253, 184, 100, 225, 124, 82, 70, 138, 222, 246, 185, 192, 129, 247, 218, 24, 26, 249, 112, 74, 225, 71, 139, 27, 50, 218, 11, 93, 238, 232, 163, 151, 68, 159, 146, 80, 133, 11, 45, 57, 245, 163, 117, 218, 11, 187, 246, 18, 147, 88, 171, 133, 216, 166, 47, 232, 246, 198, 170, 99, 36, 120, 114, 73, 207, 19, 218, 202, 146, 158, 223, 107, 234, 171, 172, 20, 189, 133, 246, 192, 248, 57, 111, 65, 65, 135, 64, 241, 99, 87, 107, 75, 40, 224, 223, 100, 53, 180, 212, 53, 200, 172, 117, 127, 248, 193, 0, 147, 167, 222, 81, 135, 158, 135, 137]
)
}
}

40
src/wasm.rs Normal file
View File

@ -0,0 +1,40 @@
//! wasm bindings to run in a browser
use crate::keys::IssuerKeyStore;
use lazy_static::lazy_static;
use std::ops::Deref;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
lazy_static! {
static ref KEY_STORE: IssuerKeyStore = IssuerKeyStore::new();
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn init() {
log("[rust] init()");
log("[rust] setting panic hook");
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
log("[rust] loading key store");
let _store = KEY_STORE.deref();
log("[rust] init done");
}
#[wasm_bindgen]
pub fn decode_ticket(ticket: String) -> Result<JsValue, String> {
log(&format!("[rust] decode_ticket called; ticket = {}", ticket));
let iks = KEY_STORE.deref();
let ticket = crate::decode_ticket(&iks, &ticket).map_err(|e| e.to_string());
let ret = serde_wasm_bindgen::to_value(&ticket)
.map_err(|e| format!("failed to serialise ticket: {}", e))?;
Ok(ret)
}
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;