import sys
from itertools import permutations
import numpy as np
import scipy.io.wavfile as wf
from scipy.signal import butter, sosfiltfilt
TONES = [1500.0, 1900.0, 2300.0, 2700.0]
SYMBOL_MS, PREAMBLE, DATA = 20, 12, 80
K_MUL, K_ADD = 0x2F6B5D41, 0x1B873593
def crc16(data):
crc = 0xFFFF
for b in data:
crc ^= b << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def keystream(coach, minutes, n):
s = (coach << 16) | minutes
out = []
for _ in range(n):
s = (s * K_MUL + K_ADD) & 0xFFFFFFFF
out.append(s >> 24)
return out
def load(path):
fs, x = wf.read(path)
x = x.astype(np.float64) / (np.iinfo(x.dtype).max if x.dtype.kind == "i" else 1)
return fs, (x[:, 0] - x[:, 1] if x.ndim == 2 else x)
def bursts(fs, band):
k = int(0.02 * fs)
env = np.convolve(np.abs(band), np.ones(k) / k, mode="same")
edges = np.flatnonzero(np.diff((env > 0.4 * env.max()).astype(int)) == 1)
out, last = [], -10**9
for e in edges:
if e - last > 0.5 * fs:
out.append(e)
last = e
return out
def demod(band, fs, start):
sym = int(round(fs * SYMBOL_MS / 1000))
def energy(p, i, f):
w = band[p + i * sym: p + (i + 1) * sym]
return abs(np.sum(w * np.exp(-2j * np.pi * f * np.arange(len(w)) / fs)))
offs = range(-int(0.004 * fs), int(0.004 * fs) + 1, 2)
off = max(offs, key=lambda o: sum(energy(start + o, i, TONES[0] if i % 2 == 0 else TONES[3]) for i in range(PREAMBLE)))
p = start + off
return [int(np.argmax([energy(p, i, f) for f in TONES])) for i in range(PREAMBLE, PREAMBLE + DATA)]
def to_bytes(digits, m):
out = bytearray()
for i in range(0, len(digits), 4):
v = 0
for d in digits[i:i + 4]:
v = (v << 2) | m[d]
out.append(v)
return bytes(out)
wav, glossary, ticket = sys.argv[1], sys.argv[2], int(sys.argv[3], 16)
words = [w.strip() for w in open(glossary) if not w.startswith("#") and w.strip()]
fs, x = load(wav)
band = sosfiltfilt(butter(6, [1200, 3000], "band", fs=fs, output="sos"), x)
for s in bursts(fs, band):
digits = demod(band, fs, s)
for m in permutations(range(4)):
rec = to_bytes(digits, m)
if rec[:2] == b"NM" and crc16(rec[:-2]) == int.from_bytes(rec[-2:], "big"):
serial = int.from_bytes(rec[3:5], "big")
coach, minutes, n = rec[9], int.from_bytes(rec[10:12], "big"), rec[12]
plain = bytes(a ^ b for a, b in zip(rec[13:13 + n], keystream(coach, minutes, n)))
flag = "rvcectf{%s_%s_%s_%02x%02x}" % (words[plain[0]], words[plain[1]], words[plain[2]], plain[3], plain[4])
print(f"{s / fs:5.1f}s serial {serial:04X}: {flag}", "<== yours" if serial == ticket else "")
break