From 19cc98c53258b8528f5eba040ecbe9d6dd9b1cd9 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Thu, 30 Jul 2026 16:43:09 +0200 Subject: [PATCH] hooked up the new DSP code. Should improve the t55xx commands --- client/CMakeLists.txt | 4 + client/Makefile | 3 + client/src/cmddata.c | 1845 +++++++++++++++++++++++++++++-- client/src/cmdflashmemspiffs.c | 15 +- client/src/cmdhf.c | 2 +- client/src/cmdlf.c | 42 +- client/src/cmdlfcotag.c | 408 +++---- client/src/cmdlfem4x70.c | 3 +- client/src/cmdlfem4x70.h | 2 +- client/src/cmdlft55xx.c | 1123 ++++++++++++++----- client/src/pm3line_vocabulary.h | 10 +- doc/commands.json | 192 +++- doc/commands.md | 18 +- 13 files changed, 2990 insertions(+), 677 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6ae302c17..43fc3288e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -461,6 +461,7 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/cmdlfsecurakey.c ${PM3_ROOT}/client/src/cmdlft55xx.c ${PM3_ROOT}/client/src/cmdlfti.c + ${PM3_ROOT}/client/src/cmdlftrovan.c ${PM3_ROOT}/client/src/cmdlfviking.c ${PM3_ROOT}/client/src/cmdlfvisa2000.c ${PM3_ROOT}/client/src/cmdlfzx8211.c @@ -491,6 +492,9 @@ set (TARGET_SOURCES ${PM3_ROOT}/client/src/pm3.c ${PM3_ROOT}/client/src/pm3_binlib.c ${PM3_ROOT}/client/src/pm3_bitlib.c + ${PM3_ROOT}/client/src/pm3_dsp.c + + ${PM3_ROOT}/client/src/pm3_fit.c ${PM3_ROOT}/client/src/pm3line.c ${PM3_ROOT}/client/src/scandir.c ${PM3_ROOT}/client/src/scripting.c diff --git a/client/Makefile b/client/Makefile index 3ad261b0b..13bb06ff7 100644 --- a/client/Makefile +++ b/client/Makefile @@ -802,6 +802,7 @@ SRCS = mifare/aiddesfire.c \ cmdlfsecurakey.c \ cmdlft55xx.c \ cmdlfti.c \ + cmdlftrovan.c \ cmdlfviking.c \ cmdlfvisa2000.c \ cmdlfzx8211.c \ @@ -888,6 +889,8 @@ SRCS = mifare/aiddesfire.c \ pm3.c \ pm3_binlib.c \ pm3_bitlib.c \ + pm3_dsp.c \ + pm3_fit.c \ preferences.c \ pm3line.c \ proxmark3.c \ diff --git a/client/src/cmddata.c b/client/src/cmddata.c index 7700c4a40..0e5de7e5a 100644 --- a/client/src/cmddata.c +++ b/client/src/cmddata.c @@ -43,7 +43,11 @@ #include "atrs.h" // ATR lookup #include "crypto/libpcrypto.h" // Cryptography #include "qrcode/qrcode.h" // QR Code lib +#include "pm3_dsp.h" // FFT, windows, spectra +#include "pm3_fit.h" // matched filter hypothesis bank +#define FITSCORE_DEFAULT_WINDOW 16384 +#define FITSCORE_MIN_SYMBOLS 128 uint8_t g_DemodBuffer[MAX_DEMOD_BUF_LEN] = { 0x00 }; size_t g_DemodBufferLen = 0; @@ -52,47 +56,6 @@ int g_DemodClock = 0; static int CmdHelp(const char *Cmd); - -// https://www.eskimo.com/~scs/c-faq.com/stdio/commaprint.html -static char *commaprint(size_t n) { - - static int comma = '\0'; - static char retbuf[30]; - - char *p = &retbuf[sizeof(retbuf) - 1]; - int i = 0; - - if (comma == '\0') { - - struct lconv *lcp = localeconv(); - if (lcp != NULL) { - - if (lcp->thousands_sep != NULL && *lcp->thousands_sep != '\0') { - comma = *lcp->thousands_sep; - } else { - comma = ','; - } - } - } - - *p = '\0'; - - do { - if (i % 3 == 0 && i != 0) { - *--p = comma; - } - - *--p = '0' + n % 10; - - n /= 10; - - i++; - - } while (n != 0); - - return p; -} - // set the g_DemodBuffer with given array ofq binary (one bit per byte) void setDemodBuff(const uint8_t *buff, size_t size, size_t start_idx) { if (buff == NULL) { @@ -121,39 +84,6 @@ bool getDemodBuff(uint8_t *buff, size_t *size) { return true; } -// include -// Root mean square -/* -static double rms(double *v, size_t n) { - double sum = 0.0; - for (size_t i = 0; i < n; i++) - sum += v[i] * v[i]; - return sqrt(sum / n); -} - -static int cmp_int(const void *a, const void *b) { - if (*(const int *)a < * (const int *)b) - return -1; - else - return *(const int *)a > *(const int *)b; -} -static int cmp_uint8(const void *a, const void *b) { - if (*(const uint8_t *)a < * (const uint8_t *)b) - return -1; - else - return *(const uint8_t *)a > *(const uint8_t *)b; -} -// Median of a array of values - -static double median_int(int *src, size_t size) { - qsort(src, size, sizeof(int), cmp_int); - return 0.5 * (src[size / 2] + src[(size - 1) / 2]); -} -static double median_uint8(uint8_t *src, size_t size) { - qsort(src, size, sizeof(uint8_t), cmp_uint8); - return 0.5 * (src[size / 2] + src[(size - 1) / 2]); -} -*/ // function to compute mean for a series static double compute_mean(const int *data, size_t n) { double mean = 0.0; @@ -163,7 +93,7 @@ static double compute_mean(const int *data, size_t n) { return mean; } -// function to compute variance for a series +// function to compute variance for a series static double compute_variance(const int *data, size_t n) { double variance = 0.0; double mean = compute_mean(data, n); @@ -175,30 +105,6 @@ static double compute_variance(const int *data, size_t n) { return variance; } -// Function to compute autocorrelation for a series -// Author: Kenneth J. Christensen -// - Corrected divide by n to divide (n - lag) from Tobias Mueller -/* -static double compute_autoc(const int *data, size_t n, int lag) { - double autocv = 0.0; // Autocovariance value - double ac_value; // Computed autocorrelation value to be returned - double variance; // Computed variance - double mean; - - mean = compute_mean(data, n); - variance = compute_variance(data, n); - - for (size_t i=0; i < (n - lag); i++) - autocv += (data[i] - mean) * (data[i+lag] - mean); - - autocv = (1.0 / (n - lag)) * autocv; - - // Autocorrelation is autocovariance divided by variance - ac_value = autocv / variance; - return ac_value; -} -*/ - static int CmdSetDebugMode(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "data setdebugmode", @@ -1176,9 +1082,68 @@ static int CmdAskEdgeDetect(const char *Cmd) { return res; } -// Print our clock rate -// uses data from graphbuffer -// adjusted to take char parameter for type of modulation to find the clock - by marshmellow. +// Clock detection for `data detectclock`. +static int detectclock_fit(int mod_mask, pm3_hyp_t *hyp) { + + if (g_GraphTraceLen < 2048) { + return PM3_ESOFT; + } + + const size_t count = (g_GraphTraceLen < FITSCORE_DEFAULT_WINDOW) + ? g_GraphTraceLen + : FITSCORE_DEFAULT_WINDOW; + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return PM3_EMALLOC; + } + + pm3_fit_opts_t opts = {0}; + opts.mod_mask = mod_mask; + + pm3_fit_t fit; + int res = pm3_fit_run(sig, count, &opts, &fit); + free(sig); + + if (res != PM3_SUCCESS) { + return res; + } + + *hyp = fit.items[0]; + pm3_fit_free(&fit); + return PM3_SUCCESS; +} + +static int detectclock_print(int mod_mask, bool verbose) { + + pm3_hyp_t hyp; + if (detectclock_fit(mod_mask, &hyp) != PM3_SUCCESS) { + return 0; + } + + const int clk = (int)(hyp.clk_fine + 0.5); + if (clk <= 0) { + return 0; + } + + if (verbose) { + if (hyp.mod == PM3_MOD_FSK) { + PrintAndLogEx(SUCCESS, "Detected Field Clocks: FC/%d, FC/%d - Bit Clock: RF/%d" + , hyp.fc_hi + , hyp.fc_lo + , clk + ); + } else if (hyp.mod == PM3_MOD_ASK) { + PrintAndLogEx(SUCCESS, "Auto-detected clock rate: %d, Best Starting Position: %d", clk, hyp.phase); + } else { + PrintAndLogEx(SUCCESS, "Auto-detected clock rate: %d", clk); + } + } + + setClockGrid(clk, hyp.phase); + return clk; +} + static int CmdDetectClockRate(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "data detectclock", @@ -1207,6 +1172,18 @@ static int CmdDetectClockRate(const char *Cmd) { return PM3_EINVARG; } else if (tmp == 0) { + // no modulation asked for, so report whichever one the signal is + pm3_hyp_t hyp; + if (detectclock_fit(0, &hyp) == PM3_SUCCESS) { + const int clk = (int)(hyp.clk_fine + 0.5); + if (clk > 0) { + PrintAndLogEx(SUCCESS, "%s Clock... %d", pm3_mod_name(hyp.mod), clk); + setClockGrid(clk, hyp.phase); + return PM3_SUCCESS; + } + } + + // nothing fit, fall back to what the old heuristics can offer int clock = GetFskClock("", false); if (clock > 0) { PrintAndLogEx(SUCCESS, "FSK Clock... %d", clock); @@ -1226,17 +1203,21 @@ static int CmdDetectClockRate(const char *Cmd) { return PM3_SUCCESS; } - if (a) + if (a && detectclock_print(1 << PM3_MOD_ASK, true) == 0) { GetAskClock("", true); + } - if (f) + if (f && detectclock_print(1 << PM3_MOD_FSK, true) == 0) { GetFskClock("", true); + } - if (n) + if (n && detectclock_print(1 << PM3_MOD_NRZ, true) == 0) { GetNrzClock("", true); + } - if (p) + if (p && detectclock_print(1 << PM3_MOD_PSK, true) == 0) { GetPskClock("", true); + } RepaintGraphWindow(); return PM3_SUCCESS; @@ -2031,7 +2012,7 @@ static int CmdLoad(const char *Cmd) { } fclose(f); - PrintAndLogEx(SUCCESS, "loaded " _YELLOW_("%s") " samples", commaprint(g_GraphTraceLen)); + PrintAndLogEx(SUCCESS, "loaded " _YELLOW_("%zu") " samples", g_GraphTraceLen); if (nofix == false) { uint8_t *bits = calloc(g_GraphTraceLen, sizeof(uint8_t)); @@ -4107,9 +4088,1642 @@ static int CmdQRcode(const char *Cmd) { return PM3_SUCCESS; } +//----------------------------------------------------------------------------- +// Frequency domain analysis +// +// These commands all read the graph buffer, never write to it unless asked, +// The transforms themselves live in pm3_dsp.c +//----------------------------------------------------------------------------- + +static size_t largest_pow2_le(size_t v) { + size_t p = 1; + while ((p << 1) <= v && (p << 1) <= PM3_DSP_MAX_FFT) { + p <<= 1; + } + return p; +} + +// Shared front end for the host side DSP commands. Validates the requested +// window against the graph buffer, rejects degenerate input, and hands back a +// mean removed, unit variance copy of the samples. Caller frees *sig. +// +// `size` == 0 means "the largest power of two that fits from start". When a +// size is given it is rounded up to a power of two for the transform length, +// which is returned in *n, while *count stays at the number of real samples. +static int dsp_prepare(int start, int size, double **sig, size_t *count, size_t *n) { + + *sig = NULL; + *count = 0; + *n = 0; + + if (g_GraphTraceLen == 0) { + PrintAndLogEx(WARNING, "GraphBuffer is empty"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data load -f ") "` or `" _YELLOW_("lf read") "` to get samples"); + return PM3_EINVARG; + } + + if (start < 0) { + PrintAndLogEx(WARNING, "start must not be negative"); + return PM3_EINVARG; + } + + if ((size_t)start >= g_GraphTraceLen) { + PrintAndLogEx(WARNING, "start ( " _YELLOW_("%d") " ) is past the end of the trace ( " _YELLOW_("%zu") " samples )" + , start + , g_GraphTraceLen + ); + return PM3_EINVARG; + } + + if (size < 0) { + PrintAndLogEx(WARNING, "size must not be negative"); + return PM3_EINVARG; + } + + const size_t avail = g_GraphTraceLen - (size_t)start; + + if (avail < 4) { + PrintAndLogEx(WARNING, "not enough samples from start ( " _YELLOW_("%zu") " ), need at least 4", avail); + return PM3_EINVARG; + } + + if (size == 0) { + *n = largest_pow2_le(avail); + *count = *n; + } else { + if ((size_t)size > avail) { + PrintAndLogEx(WARNING, "size ( " _YELLOW_("%d") " ) is larger than the %zu samples available from start", size, avail); + return PM3_EINVARG; + } + if ((size_t)size > PM3_DSP_MAX_FFT) { + PrintAndLogEx(WARNING, "size ( " _YELLOW_("%d") " ) exceeds the maximum transform length of %d", size, PM3_DSP_MAX_FFT); + return PM3_EINVARG; + } + *count = (size_t)size; + *n = pm3_next_pow2(*count); + } + + if (*n < 4) { + PrintAndLogEx(WARNING, "transform length ( " _YELLOW_("%zu") " ) is too short to be useful", *n); + return PM3_EINVARG; + } + + // a flat window carries no information and every downstream statistic + // would divide by zero. Catch it here rather than printing NaN tables. + bool flat = true; + for (size_t i = 1; i < *count; i++) { + if (g_GraphBuffer[start + i] != g_GraphBuffer[start]) { + flat = false; + break; + } + } + if (flat) { + PrintAndLogEx(WARNING, "all %zu samples in the window are identical, nothing to analyse", *count); + return PM3_EINVARG; + } + + *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, *count); + if (*sig == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return PM3_EMALLOC; + } + + return PM3_SUCCESS; +} + +// Frequencies are reported in cycles/sample as the primary unit. Hz is only +// printed when the user tells us the sample rate with --fs: the client keeps no +// record of the divisor a trace was captured with, and assuming 125 kHz would +// silently mislabel every HF and every non default LF capture. +static int CmdFFT(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data fft", + "Fourier transform of the samples in the GraphBuffer.\n" + "Reports the magnitude spectrum of the first N/2+1 bins, the input being real.\n" + "The DC component is removed and the window is normalised to unit variance\n" + "before the transform, so magnitudes are comparable between captures.", + "data fft --> transform the whole buffer\n" + "data fft --size 4096 --win blackman --> 4096 point transform, blackman window\n" + "data fft --start 1000 --size 8192 --db --> magnitudes in dB relative to the peak\n" + "data fft --size 4096 --graph --> put the spectrum in the graph window\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "transform length, rounded up to a power of two (def largest that fits)"), + arg_str0(NULL, "win", "", "window function (def hann)"), + arg_lit0(NULL, "db", "report magnitude in dB relative to the peak bin instead of linear"), + arg_lit0(NULL, "graph", "write the magnitude spectrum into the GraphBuffer and repaint"), + arg_int0(NULL, "fs", "", "sample rate in Hz, adds a frequency column in Hz"), + arg_int0(NULL, "bins", "", "how many bins to print (def 32, 0 for all)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char win_str[16] = {0}; + int win_len = sizeof(win_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)win_str, win_len, &win_len); + + bool use_db = arg_get_lit(ctx, 4); + bool to_graph = arg_get_lit(ctx, 5); + int fs = arg_get_int_def(ctx, 6, 0); + int want_bins = arg_get_int_def(ctx, 7, 32); + CLIParserFree(ctx); + + pm3_window_t win = PM3_WIN_HANN; + if (win_len > 0 && pm3_window_from_str(win_str, &win) == false) { + PrintAndLogEx(WARNING, "unknown window `%s`, expected hann, hamming, blackman or rect", win_str); + return PM3_EINVARG; + } + + if (want_bins < 0) { + PrintAndLogEx(WARNING, "bins must not be negative"); + return PM3_EINVARG; + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_spectrum_t spec; + res = pm3_spectrum(sig, count, n, win, &spec); + free(sig); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "failed to compute the spectrum"); + return res; + } + + const double bin_width = 1.0 / (double)n; + const double peak_freq = (double)spec.peak_bin * bin_width; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Fourier transform") " ---------------------------------"); + PrintAndLogEx(INFO, " samples....... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " transform..... " _YELLOW_("%zu") " point, %zu bins reported", n, spec.nbins); + PrintAndLogEx(INFO, " window........ " _YELLOW_("%s"), pm3_window_name(win)); + PrintAndLogEx(INFO, " bin width..... " _YELLOW_("%.6f") " cycles/sample", bin_width); + + if (fs > 0) { + PrintAndLogEx(INFO, " sample rate... " _YELLOW_("%d") " Hz ( %.2f Hz per bin )", fs, bin_width * (double)fs); + } else { + PrintAndLogEx(INFO, " sample rate... " _YELLOW_("unknown") ", use `--fs` to add a Hz column"); + } + + PrintAndLogEx(INFO, " peak bin...... " _GREEN_("%zu") " ( %.6f cycles/sample, clock %.2f samples/symbol )" + , spec.peak_bin + , peak_freq + , (peak_freq > 0.0) ? 1.0 / peak_freq : 0.0 + ); + if (fs > 0) { + PrintAndLogEx(INFO, " peak.......... " _GREEN_("%.1f") " Hz", peak_freq * (double)fs); + } + + size_t print_bins = (want_bins == 0) ? spec.nbins : (size_t)want_bins; + if (print_bins > spec.nbins) { + print_bins = spec.nbins; + } + + if (print_bins) { + + PrintAndLogEx(NORMAL, ""); + if (fs > 0) { + PrintAndLogEx(INFO, " bin cycles/sample Hz %s", use_db ? " mag (dB)" : " magnitude"); + PrintAndLogEx(INFO, "------+--------------+-----------+-----------"); + } else { + PrintAndLogEx(INFO, " bin cycles/sample %s", use_db ? " mag (dB)" : " magnitude"); + PrintAndLogEx(INFO, "------+--------------+-----------"); + } + + for (size_t i = 0; i < print_bins; i++) { + + const double f = (double)i * bin_width; + char val[32]; + + if (use_db) { + snprintf(val, sizeof(val), "%9.2f", 20.0 * log10((spec.mag[i] / (spec.peak + 1e-30)) + 1e-30)); + } else { + snprintf(val, sizeof(val), "%9.3f", spec.mag[i]); + } + + if (fs > 0) { + PrintAndLogEx(INFO, "%5zu | %12.6f | %9.1f | %s", i, f, f * (double)fs, val); + } else { + PrintAndLogEx(INFO, "%5zu | %12.6f | %s", i, f, val); + } + } + + if (print_bins < spec.nbins) { + PrintAndLogEx(INFO, "( %zu of %zu bins shown, use `--bins 0` for all )", print_bins, spec.nbins); + } + } + + if (to_graph) { + + // dB against the peak keeps the low level structure visible, a linear + // plot of an LF spectrum is one spike and a flat line + for (size_t i = 0; i < spec.nbins; i++) { + double db = 20.0 * log10((spec.mag[i] / (spec.peak + 1e-30)) + 1e-30); + if (db < -96.0) { + db = -96.0; + } + g_GraphBuffer[i] = (int32_t)((db + 96.0) * (255.0 / 96.0)) - 128; + } + g_GraphTraceLen = spec.nbins; + + // the x axis is now bins, not samples. Drive the existing cursor + // scale so a marker delta reads out directly in cycles/sample. + setClockGrid(0, 0); + g_CursorScaleFactor = (double)n; + snprintf(g_CursorScaleFactorUnit, sizeof(g_CursorScaleFactorUnit), "c/sample"); + RepaintGraphWindow(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, "GraphBuffer now holds a " _YELLOW_("magnitude spectrum")); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data load -f ") "` to get the original samples back"); + } + + pm3_spectrum_free(&spec); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +static void print_peak_table(const char *title, const pm3_peak_t *peaks, size_t n, int fs) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "%s", title); + + if (n == 0) { + PrintAndLogEx(INFO, " no peaks found above the noise floor"); + return; + } + + if (fs > 0) { + PrintAndLogEx(INFO, " bin cycles/sample Hz clock mag(dB) prom(dB)"); + PrintAndLogEx(INFO, "------+--------------+-----------+---------+----------+----------"); + } else { + PrintAndLogEx(INFO, " bin cycles/sample clock mag(dB) prom(dB)"); + PrintAndLogEx(INFO, "------+--------------+---------+----------+----------"); + } + + for (size_t i = 0; i < n; i++) { + if (fs > 0) { + PrintAndLogEx(INFO, "%5zu | %12.6f | %9.1f | %7.2f | %8.2f | %8.2f" + , peaks[i].bin + , peaks[i].freq + , peaks[i].freq * (double)fs + , peaks[i].clk + , peaks[i].mag_db + , peaks[i].prominence + ); + } else { + PrintAndLogEx(INFO, "%5zu | %12.6f | %7.2f | %8.2f | %8.2f" + , peaks[i].bin + , peaks[i].freq + , peaks[i].clk + , peaks[i].mag_db + , peaks[i].prominence + ); + } + } +} + +// Pick the analysis window when the user did not. A capture long enough to +// have a choice gets one cheap look at its spectrum first, because the right +// window depends on the symbol rate and nothing knows that yet. The spectral +// estimate can land on a harmonic of the true rate, so size for twice what it +// reports rather than for exactly it. +static int autodemod_window(int start, size_t avail) { + + size_t want = MIN(avail, (size_t)FITSCORE_DEFAULT_WINDOW); + + if (avail <= (size_t)FITSCORE_DEFAULT_WINDOW) { + return (int)want; + } + + double *probe = NULL; + size_t count = 0, n = 0; + if (dsp_prepare(start, (int)want, &probe, &count, &n) != PM3_SUCCESS) { + return (int)want; + } + + pm3_spec_analysis_t an; + const int res = pm3_analyse(probe, count, n, PM3_WIN_HANN, &an); + free(probe); + + if (res != PM3_SUCCESS || an.have_symbol_clk == false) { + return (int)want; + } + + const double need = an.symbol_clk * 2.0 * (double)FITSCORE_MIN_SYMBOLS; + if (need > (double)want) { + want = MIN(avail, (size_t)need); + want = MIN(want, (size_t)PM3_DSP_MAX_FFT); + } + + return (int)want; +} + +// Keep a copy of the graph buffer the first time something is about to +// overwrite it. `data autodemod` rectifies and resamples in place so the +// demodulators see what it decided on, and both have to be undoable. +static void autodemod_backup(int32_t **backup, size_t len) { + + if (*backup != NULL || len == 0) { + return; + } + + *backup = calloc(len, sizeof(int32_t)); + if (*backup != NULL) { + memcpy(*backup, g_GraphBuffer, len * sizeof(int32_t)); + } +} + +// Write `len` doubles into the graph buffer, scaled to the +/-100 the +// demodulators expect from a loaded trace. +static void autodemod_store(const double *src, size_t len) { + + double peak = 0.0; + for (size_t i = 0; i < len; i++) { + if (fabs(src[i]) > peak) { + peak = fabs(src[i]); + } + } + + if (peak <= 0.0) { + peak = 1.0; + } + + for (size_t i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((src[i] / peak) * 100.0); + } + g_GraphTraceLen = len; +} + +static void print_family_hint(const pm3_spec_analysis_t *an) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Modulation family") " ---------------------------------"); + + if (an->needs_envelope) { + PrintAndLogEx(INFO, " carrier....... rf/" _YELLOW_("%.2f") ", switched on and off ( depth " _YELLOW_("%.2f") " )" + , an->carrier_clk + , an->envelope_depth + ); + PrintAndLogEx(INFO, " two samples per carrier period, so the data is in the envelope, not the phase"); + PrintAndLogEx(INFO, " everything below was measured on the rectified envelope"); + } + + if (an->family == PM3_FAM_UNKNOWN) { + PrintAndLogEx(WARNING, "no clear spectral signature, confidence " _RED_("none")); + PrintAndLogEx(INFO, "the capture may be noise, or the symbol rate may be outside the search range"); + return; + } + + if (an->confidence >= PM3_CONF_MEDIUM) { + PrintAndLogEx(SUCCESS, "looks like " _GREEN_("%s") ", confidence " _GREEN_("%s") + , pm3_family_name(an->family) + , pm3_confidence_name(an->confidence) + ); + } else { + PrintAndLogEx(WARNING, "looks like " _YELLOW_("%s") ", confidence " _YELLOW_("%s") + , pm3_family_name(an->family) + , pm3_confidence_name(an->confidence) + ); + } + + switch (an->family) { + case PM3_FAM_FSK: + PrintAndLogEx(INFO, " field clocks.. rf/" _YELLOW_("%.2f") " and rf/" _YELLOW_("%.2f") " ( ratio %.3f )" + , an->fsk_clk_hi + , an->fsk_clk_lo + , an->fsk_ratio + ); + PrintAndLogEx(INFO, " two strong lines at a stable ratio is the FSK signature"); + break; + case PM3_FAM_PSK: + PrintAndLogEx(INFO, " subcarrier.... rf/" _YELLOW_("%.2f"), an->carrier_clk); + PrintAndLogEx(INFO, " carrier is suppressed at the symbol rate, the line only appears after squaring"); + break; + case PM3_FAM_MANCHESTER: + PrintAndLogEx(INFO, " null at DC, energy concentrated at the symbol rate"); + break; + case PM3_FAM_ASK: + PrintAndLogEx(INFO, " a line at the symbol rate survives in the plain spectrum"); + break; + case PM3_FAM_NRZ: + PrintAndLogEx(INFO, " maximum at DC with a sinc squared skirt"); + break; + case PM3_FAM_UNKNOWN: + break; + } + + if (an->have_symbol_clk) { + PrintAndLogEx(INFO, " symbol clock.. " _GREEN_("%.2f") " samples/symbol", an->symbol_clk); + const double frac = fabs(an->symbol_clk - floor(an->symbol_clk + 0.5)); + if (frac > 0.05) { + PrintAndLogEx(WARNING, " clock is " _YELLOW_("%.2f") " samples off the nearest integer", frac); + PrintAndLogEx(INFO, " a fractional clock decodes cleanly at the start of a capture and drifts out later"); + } + } + + PrintAndLogEx(INFO, " this is a hint from spectral shape alone, no bits have been decoded"); +} + +// Renders the spectrogram as a character ramp. Rows are frequency bands, +// columns are time. Both are folded to fit a terminal. +static void print_heatmap(const pm3_stft_t *st) { + + static const char ramp[] = " .:-=+*#%@"; + const size_t nramp = sizeof(ramp) - 2; + + const size_t cols = (st->nframes < 96) ? st->nframes : 96; + const size_t rows = 20; + + // only the lower part of the spectrum carries the symbol rate, the top + // octave is almost always empty on an LF capture + const size_t band_hi = st->nbins; + + double peak = 0.0; + for (size_t i = 0; i < st->nframes * st->nbins; i++) { + if (st->ridge[i] > peak) { + peak = st->ridge[i]; + } + } + if (peak <= 0.0) { + return; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Spectrogram") " ---------------------------------------"); + PrintAndLogEx(INFO, "rows are frequency ( top = Nyquist ), columns are time, `%c` is the peak", ramp[nramp]); + + char line[128]; + + for (size_t r = 0; r < rows; r++) { + + // row 0 is the top of the spectrum + const size_t b_lo = ((rows - 1 - r) * band_hi) / rows; + size_t b_hi = ((rows - r) * band_hi) / rows; + if (b_hi <= b_lo) { + b_hi = b_lo + 1; + } + + for (size_t c = 0; c < cols; c++) { + + const size_t f_lo = (c * st->nframes) / cols; + size_t f_hi = ((c + 1) * st->nframes) / cols; + if (f_hi <= f_lo) { + f_hi = f_lo + 1; + } + + double best = 0.0; + for (size_t f = f_lo; f < f_hi && f < st->nframes; f++) { + for (size_t b = b_lo; b < b_hi && b < st->nbins; b++) { + const double v = st->ridge[(f * st->nbins) + b]; + if (v > best) { + best = v; + } + } + } + + size_t idx = (size_t)((best / peak) * (double)nramp); + if (idx > nramp) { + idx = nramp; + } + line[c] = ramp[idx]; + } + line[cols] = '\0'; + + const double f_top = (double)b_hi / (double)st->n; + PrintAndLogEx(INFO, "%8.4f |%s|", f_top, line); + } + + PrintAndLogEx(INFO, " +%.*s+", (int)cols, "--------------------------------------------------------------------------------------------------"); + PrintAndLogEx(INFO, " sample %-*zu%zu", (int)(cols - 8), st->frame_start[0], st->frame_start[st->nframes - 1] + st->n); +} + +static void report_stft(const pm3_stft_t *st, int start) { + + print_heatmap(st); + + // median of the dominant rate, used as the reference the ridge must stay close to + double *sorted = calloc(st->nframes, sizeof(double)); + if (sorted == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return; + } + memcpy(sorted, st->peak_freq, st->nframes * sizeof(double)); + for (size_t i = 1; i < st->nframes; i++) { + const double v = sorted[i]; + size_t j = i; + while (j > 0 && sorted[j - 1] > v) { + sorted[j] = sorted[j - 1]; + j--; + } + sorted[j] = v; + } + const double median = sorted[st->nframes / 2]; + + memcpy(sorted, st->peak_mag, st->nframes * sizeof(double)); + for (size_t i = 1; i < st->nframes; i++) { + const double v = sorted[i]; + size_t j = i; + while (j > 0 && sorted[j - 1] > v) { + sorted[j] = sorted[j - 1]; + j--; + } + sorted[j] = v; + } + const double median_mag = sorted[st->nframes / 2]; + free(sorted); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Ridge") " ---------------------------------------------"); + + if (median <= 0.0) { + PrintAndLogEx(WARNING, "no coherent rate found in any frame"); + return; + } + + // longest run of frames whose dominant rate stays within 5% of the median + size_t best_lo = 0, best_len = 0, run_lo = 0, run_len = 0; + for (size_t f = 0; f < st->nframes; f++) { + + // a frame counts as coherent when the clock is where we expect it and + // there is actually something there. Frames before the tag enters the + // field pass the first test on noise alone, hence the second. + const bool ok = ((fabs(st->peak_freq[f] - median) / median) < 0.10) + && (st->peak_mag[f] > (0.25 * median_mag)); + if (ok) { + if (run_len == 0) { + run_lo = f; + } + run_len++; + if (run_len > best_len) { + best_len = run_len; + best_lo = run_lo; + } + } else { + run_len = 0; + } + } + + if (best_len == 0) { + PrintAndLogEx(WARNING, "the dominant rate never settles, the capture may be all noise"); + return; + } + + const size_t s_lo = start + st->frame_start[best_lo]; + const size_t s_hi = start + st->frame_start[best_lo + best_len - 1] + st->n; + + PrintAndLogEx(INFO, " coherent...... samples " _GREEN_("%zu") " .. " _GREEN_("%zu") " ( %zu of %zu frames )" + , s_lo + , s_hi + , best_len + , st->nframes + ); + + if (best_len < st->nframes) { + if (s_hi < g_GraphTraceLen) { + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data rtrim -i %zu") "` to drop the incoherent tail", s_hi); + } + if (s_lo > 0) { + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data ltrim -i %zu") "` to drop the incoherent head", s_lo); + } + } else { + PrintAndLogEx(INFO, " the whole capture holds a coherent clock, no trimming needed"); + } + + // drift, measured over the coherent span only + const size_t edge = (best_len / 10) ? (best_len / 10) : 1; + + double f_start = 0.0, f_end = 0.0; + for (size_t i = 0; i < edge; i++) { + f_start += st->peak_freq[best_lo + i]; + f_end += st->peak_freq[best_lo + best_len - 1 - i]; + } + + f_start /= (double)edge; + f_end /= (double)edge; + + if (f_start > 0.0 && f_end > 0.0) { + + const double clk_start = 1.0 / f_start; + const double clk_end = 1.0 / f_end; + const double pct = ((clk_end - clk_start) / clk_start) * 100.0; + + PrintAndLogEx(INFO, " clock drift... " _YELLOW_("%.2f") " -> " _YELLOW_("%.2f") " samples/symbol ( %+.2f %% )" + , clk_start + , clk_end + , pct + ); + + if (fabs(pct) > 1.0) { + PrintAndLogEx(WARNING, " the clock moves across the capture, a single integer clock will not fit all of it"); + } + } + + // discontinuities + size_t jumps = 0; + for (size_t f = 1; f < st->nframes; f++) { + const double d = fabs(st->peak_freq[f] - st->peak_freq[f - 1]); + if ((d / median) > 0.15) { + if (jumps < 8) { + PrintAndLogEx(INFO, " rate jump..... at sample " _YELLOW_("%zu") " ( %.2f -> %.2f samples/symbol )" + , (size_t)start + st->frame_start[f] + , (st->peak_freq[f - 1] > 0.0) ? 1.0 / st->peak_freq[f - 1] : 0.0 + , (st->peak_freq[f] > 0.0) ? 1.0 / st->peak_freq[f] : 0.0 + ); + } + jumps++; + } + } + if (jumps > 8) { + PrintAndLogEx(INFO, " ( %zu more jumps not shown )", jumps - 8); + } + if (jumps == 0) { + PrintAndLogEx(INFO, " rate jumps.... " _GREEN_("none")); + } +} + +static int CmdSpectrum(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data spectrum", + "Spectral analysis of the GraphBuffer.\n" + "Reports the strongest spectral peaks with a sub bin estimate of the symbol clock,\n" + "the squaring and delay-and-multiply spectra which expose rates the plain spectrum\n" + "nulls out, and a modulation family hint derived from spectral shape alone.\n" + "Run this on a trace that will not decode.", + "data spectrum --> peak table and family hint\n" + "data spectrum --top 8 --sq --> more peaks, plus the squaring spectrum\n" + "data spectrum --stft --size 2048 --> spectrogram over the whole capture\n" + "data spectrum --stft --size 2048 --hop 256 --> finer time resolution\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "transform length, rounded up to a power of two (def largest that fits)"), + arg_str0(NULL, "win", "", "window function (def hann)"), + arg_int0(NULL, "top", "", "number of peaks to report (def 5)"), + arg_lit0(NULL, "sq", "also show the squaring and delay-and-multiply spectra"), + arg_lit0(NULL, "stft", "sliding window spectrogram, ridge tracking and drift"), + arg_int0(NULL, "hop", "", "STFT hop in samples (def size/4)"), + arg_int0(NULL, "fs", "", "sample rate in Hz, adds a frequency column in Hz"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char win_str[16] = {0}; + int win_len = sizeof(win_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)win_str, win_len, &win_len); + + int top = arg_get_int_def(ctx, 4, 5); + bool show_sq = arg_get_lit(ctx, 5); + bool do_stft = arg_get_lit(ctx, 6); + int hop = arg_get_int_def(ctx, 7, 0); + int fs = arg_get_int_def(ctx, 8, 0); + CLIParserFree(ctx); + + pm3_window_t win = PM3_WIN_HANN; + if (win_len > 0 && pm3_window_from_str(win_str, &win) == false) { + PrintAndLogEx(WARNING, "unknown window `%s`, expected hann, hamming, blackman or rect", win_str); + return PM3_EINVARG; + } + + if (top < 1 || top > PM3_DSP_MAX_PEAKS) { + PrintAndLogEx(WARNING, "top must be between 1 and %d", PM3_DSP_MAX_PEAKS); + return PM3_EINVARG; + } + + if (hop < 0) { + PrintAndLogEx(WARNING, "hop must not be negative"); + return PM3_EINVARG; + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_spec_analysis_t an; + res = pm3_analyse(sig, count, n, win, &an); + if (res != PM3_SUCCESS) { + free(sig); + PrintAndLogEx(WARNING, "spectral analysis failed"); + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Spectrum") " ------------------------------------------"); + PrintAndLogEx(INFO, " samples..... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " transform... " _YELLOW_("%zu") " point, %s window", n, pm3_window_name(win)); + PrintAndLogEx(INFO, " bin width... " _YELLOW_("%.6f") " cycles/sample", 1.0 / (double)n); + + print_peak_table("--- " _CYAN_("Peaks") " ---------------------------------------------", an.peaks, MIN(an.npeaks, (size_t)top), fs); + PrintAndLogEx(INFO, "clock is the reciprocal of the frequency"); + + if (show_sq) { + + print_peak_table("--- " _CYAN_("Squaring spectrum") " ( y = x * x ) -------------", an.sq_peaks, MIN(an.nsq_peaks, (size_t)top), fs); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Delay and multiply") " ( y[i] = x[i] * x[i-d] ) ---"); + PrintAndLogEx(INFO, " lag clock prom(dB)"); + PrintAndLogEx(INFO, "------+---------+----------"); + for (size_t i = 0; i < an.ndelay; i++) { + if (an.delay_valid[i] == false) { + continue; + } + PrintAndLogEx(INFO, "%5zu | %7.2f | %8.2f", an.delay[i], an.delay_peak[i].clk, an.delay_peak[i].prominence); + } + PrintAndLogEx(INFO, "squaring a BPSK subcarrier collapses the +/-180 modulation into a tone at twice the carrier"); + } + + print_family_hint(&an); + + if (do_stft) { + + size_t stft_hop = (hop > 0) ? (size_t)hop : (n / 4); + if (stft_hop == 0) { + stft_hop = 1; + } + + // the spectrogram wants the whole trace from start + const size_t avail = g_GraphTraceLen - (size_t)start; + double *full = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, avail); + + if (full == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + free(sig); + return PM3_EMALLOC; + } + + // anchor the ridge tracker on the symbol rate the full length analysis found, + // so drift is measured against the clock we care about rather than against whichever bin peaked in each frame + const double anchor = an.have_symbol_clk ? (1.0 / an.symbol_clk) : 0.0; + + pm3_stft_t st; + res = pm3_stft(full, avail, n, stft_hop, win, anchor, &st); + free(full); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "cannot build a spectrogram: need at least one full %zu sample window from offset %d", n, start); + PrintAndLogEx(HINT, "Hint: Try a smaller `" _YELLOW_("--size") "`"); + } else { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, " frames........ " _YELLOW_("%zu") " of %zu samples, hop " _YELLOW_("%zu"), st.nframes, st.n, st.hop); + report_stft(&st, start); + pm3_stft_free(&st); + } + } + + free(sig); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +static int parse_mod_mask(const char *str, int *mask) { + + *mask = 0; + if (str == NULL || str[0] == '\0') { + return PM3_SUCCESS; + } + + if (strcmp(str, "ask") == 0) { + *mask = 1 << PM3_MOD_ASK; + } else if (strcmp(str, "fsk") == 0) { + *mask = 1 << PM3_MOD_FSK; + } else if (strcmp(str, "psk") == 0) { + *mask = 1 << PM3_MOD_PSK; + } else if (strcmp(str, "nrz") == 0) { + *mask = 1 << PM3_MOD_NRZ; + } else { + return PM3_EINVARG; + } + return PM3_SUCCESS; +} + +static void print_fit_table(const pm3_fit_t *fit, size_t rows) { + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "rank mod encoding clk clk fine Q phase SNR_dd eye mid margin"); + PrintAndLogEx(INFO, "-----+-----+------------+-----+----------+----+-------+---------+-------+------+--------"); + + for (size_t i = 0; i < rows; i++) { + + const pm3_hyp_t *h = &fit->items[i]; + + char extra[24] = {0}; + if (h->tpl == PM3_TPL_FSK) { + snprintf(extra, sizeof(extra), " fc %d/%d", h->fc_hi, h->fc_lo); + } else if (h->tpl == PM3_TPL_PSK) { + snprintf(extra, sizeof(extra), " fc %d", h->fc); + } + + PrintAndLogEx(INFO, "%4zu | %-3s | %-10s | %3d | %8.2f | %2d | %5d | %7.2f | %5.3f | %4.2f | %6.2f%s" + , i + 1 + , pm3_mod_name(h->mod) + , pm3_enc_name(h->enc) + , h->clk + , h->clk_fine + , h->q + , h->phase + , h->snr_dd + , h->eye + , h->mid_ratio + , h->margin + , extra + ); + } +} + +static int CmdFitScore(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data fitscore", + "Rank matched filter hypotheses against the GraphBuffer.\n" + "A hypothesis is a modulation, a symbol shape and a clock. Each one is scored by\n" + "correlating the trace against a single symbol template and measuring how tightly\n" + "the correlator output clusters at the decision instants. Phase is not searched:\n" + "the correlation is done with an FFT, which yields every phase offset at once.\n" + "Templates are shaped by an assumed antenna Q, because correlating a bandlimited\n" + "signal against square edges biases the ranking toward short clocks.", + "data fitscore --> rank the whole bank\n" + "data fitscore --mod ask --top 5 --> ASK hypotheses only\n" + "data fitscore --clk 64 --all --> everything at clock 64\n" + "data fitscore --verbose --> also put the rank 1 correlator output in the graph\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "samples to analyse, rounded up to a power of two (def 16384)"), + arg_str0(NULL, "mod", "", "restrict to one modulation (def all)"), + arg_int0(NULL, "clk", "", "restrict to one clock (def all candidates)"), + arg_int0(NULL, "top", "", "rows to print (def 10)"), + arg_lit0(NULL, "all", "print every scored hypothesis"), + arg_lit0("v", "verbose", "write the rank 1 correlator output into the GraphBuffer"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + + char mod_str[8] = {0}; + int mod_len = sizeof(mod_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)mod_str, mod_len, &mod_len); + + int clk_only = arg_get_int_def(ctx, 4, 0); + int top = arg_get_int_def(ctx, 5, 10); + bool show_all = arg_get_lit(ctx, 6); + bool verbose = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + pm3_fit_opts_t opts = {0}; + + if (parse_mod_mask(mod_str, &opts.mod_mask) != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "unknown modulation `%s`, expected ask, fsk, psk or nrz", mod_str); + return PM3_EINVARG; + } + + if (clk_only < 0) { + PrintAndLogEx(WARNING, "clk must not be negative"); + return PM3_EINVARG; + } + if (clk_only > 0) { + const int *clocks = NULL; + const size_t nclk = pm3_fit_clocks(&clocks); + bool known = false; + for (size_t i = 0; i < nclk; i++) { + if (clocks[i] == clk_only) { + known = true; + break; + } + } + if (known == false) { + PrintAndLogEx(WARNING, "clock " _YELLOW_("%d") " is not one of the candidates the detectors search", clk_only); + PrintAndLogEx(INFO, "candidates are:"); + char buf[128] = {0}; + for (size_t i = 0; i < nclk; i++) { + snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%s%d", i ? ", " : " ", clocks[i]); + } + PrintAndLogEx(INFO, "%s", buf); + return PM3_EINVARG; + } + } + opts.clk_only = clk_only; + opts.keep_corr = verbose; + + if (top < 1) { + PrintAndLogEx(WARNING, "top must be at least 1"); + return PM3_EINVARG; + } + + if (size == 0) { + const size_t avail = (g_GraphTraceLen > (size_t)MAX(start, 0)) ? g_GraphTraceLen - (size_t)MAX(start, 0) : 0; + size = autodemod_window(start, avail); + if (size <= 0) { + size = 0; + } + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + pm3_fit_t fit; + res = pm3_fit_run(sig, count, &opts, &fit); + free(sig); + + if (res == PM3_ESOFT) { + PrintAndLogEx(WARNING, "no hypothesis had enough symbols to score"); + PrintAndLogEx(INFO, "a hypothesis needs at least %d decision instants, so a clock of C needs %d*C samples" + , PM3_FIT_MIN_SYMBOLS + , PM3_FIT_MIN_SYMBOLS + ); + return res; + } + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "failed to score the hypothesis bank"); + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Matched filter bank") " -------------------------------"); + PrintAndLogEx(INFO, " samples....... " _YELLOW_("%zu") " from offset " _YELLOW_("%d"), count, start); + PrintAndLogEx(INFO, " hypotheses.... " _YELLOW_("%zu") " scored", fit.count); + + if (fit.stats.valid) { + PrintAndLogEx(INFO, " short runs.... " _YELLOW_("%.3f") " of runs are under 2.5 chips", fit.stats.short_fraction); + PrintAndLogEx(INFO, " chip.......... " _YELLOW_("%.1f") " samples, longest run " _YELLOW_("%.1f") " ( ratio %.2f, %s )" + , fit.stats.chip + , fit.stats.long_run + , fit.stats.run_ratio + , fit.stats.transition_coded ? "transition coded" : "raw levels" + ); + if (fit.stats.period > 0.0) { + PrintAndLogEx(INFO, " repeats every. " _YELLOW_("%.1f") " samples ( autocorrelation %.2f )" + , fit.stats.period + , fit.stats.period_strength + ); + } else { + PrintAndLogEx(INFO, " repeats every. " _YELLOW_("no clear period")); + } + } + + size_t rows = show_all ? fit.count : MIN((size_t)top, fit.count); + print_fit_table(&fit, rows); + + if (rows < fit.count) { + PrintAndLogEx(INFO, "( %zu of %zu rows shown, use `--all` for everything )", rows, fit.count); + } + + const pm3_hyp_t *best = &fit.items[0]; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(SUCCESS, "best fit... " _GREEN_("%s / %s") " at clock " _GREEN_("%.2f") ", phase %d, Q %d" + , pm3_mod_name(best->mod) + , pm3_enc_name(best->enc) + , best->clk_fine + , best->phase + , best->q + ); + PrintAndLogEx(INFO, " SNR_dd........ " _YELLOW_("%.2f") " dB over %zu decision instants", best->snr_dd, best->nsym); + PrintAndLogEx(INFO, " eye opening... " _YELLOW_("%.3f") " ( min / mean at the decision instants )", best->eye); + PrintAndLogEx(INFO, " mid symbol.... " _YELLOW_("%.2f") " ( relative to the decision instants )", best->mid_ratio); + + if (best->margin >= 6.0) { + PrintAndLogEx(SUCCESS, " confidence.... " _GREEN_("%.2f") " dB ahead of the next different decode", best->margin); + } else if (best->margin >= 3.0) { + PrintAndLogEx(INFO, " confidence.... " _YELLOW_("%.2f") " dB ahead of the next different decode", best->margin); + } else { + PrintAndLogEx(WARNING, " confidence.... " _RED_("%.2f") " dB ahead of the next different decode, this is ambiguous", best->margin); + } + + PrintAndLogEx(INFO, ""); + + if (verbose && fit.corr != NULL) { + + size_t len = MIN(fit.corr_len, (size_t)MAX_GRAPH_TRACE_LEN); + + double peak = 0.0; + for (size_t i = 0; i < len; i++) { + if (fit.corr[i] > peak) { + peak = fit.corr[i]; + } + } + + if (peak > 0.0) { + for (size_t i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((fit.corr[i] / peak) * 127.0); + } + g_GraphTraceLen = len; + setClockGrid(best->clk, best->phase); + RepaintGraphWindow(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, "GraphBuffer now holds the " _YELLOW_("rank 1 correlator output") ", not the original samples"); + PrintAndLogEx(INFO, "the grid is set to the winning clock and phase"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data plot") "`"); + } + } + + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +// Developer helper. +// Fills the GraphBuffer with a synthetic waveform whose modulation, encoding and clock are known +static int CmdGenSignal(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data gensignal", + "Developer tool. Fill the GraphBuffer with a synthetic LF waveform.\n" + "Used with `data fitscore` and `data autodemod`", + "data gensignal --mod ask --enc manchester --clk 64\n" + "data gensignal --mod ask --enc manchester --clk 64.25 --noise 0.4 --len 20000\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "mod", "", "modulation (def ask)"), + arg_str0(NULL, "enc", "", "encoding (def raw)"), + arg_dbl0(NULL, "clk", "", "clock in samples/symbol, may be fractional (def 64)"), + arg_int0(NULL, "len", "", "samples to generate (def 16384)"), + arg_dbl0(NULL, "noise", "", "gaussian noise standard deviation (def 0)"), + arg_dbl0(NULL, "drift", "", "linear DC ramp across the capture (def 0)"), + arg_dbl0(NULL, "env", "", "amplitude envelope swing (def 0)"), + arg_dbl0(NULL, "jitter", "", "per symbol edge jitter, samples (def 0)"), + arg_dbl0(NULL, "clip", "", "hard clip level, 0 for none (def 0)"), + arg_int0(NULL, "fc", "", "PSK subcarrier period (def 4)"), + arg_int0(NULL, "fchigh", "", "FSK long field clock (def 10)"), + arg_int0(NULL, "fclow", "", "FSK short field clock (def 8)"), + arg_int0(NULL, "q", "", "antenna Q to shape the waveform with (def 8)"), + arg_int0(NULL, "repeat", "", "loop a message of this many bits, like a real tag (def 0, random)"), + arg_int0(NULL, "seed", "", "RNG seed, for reproducible runs (def 1)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + char mod_str[8] = {0}; + int mod_len = sizeof(mod_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)mod_str, mod_len, &mod_len); + + char enc_str[16] = {0}; + int enc_len = sizeof(enc_str) - 1; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)enc_str, enc_len, &enc_len); + + pm3_gen_opts_t opts = {0}; + opts.mod = PM3_MOD_ASK; + opts.enc = PM3_ENC_RAW; + opts.clk = arg_get_dbl_def(ctx, 3, 64.0); + int len = arg_get_int_def(ctx, 4, 16384); + opts.noise = arg_get_dbl_def(ctx, 5, 0.0); + opts.drift = arg_get_dbl_def(ctx, 6, 0.0); + opts.envelope = arg_get_dbl_def(ctx, 7, 0.0); + opts.jitter = arg_get_dbl_def(ctx, 8, 0.0); + opts.clip = arg_get_dbl_def(ctx, 9, 0.0); + opts.fc = arg_get_int_def(ctx, 10, 4); + opts.fc_hi = arg_get_int_def(ctx, 11, 10); + opts.fc_lo = arg_get_int_def(ctx, 12, 8); + opts.q = arg_get_int_def(ctx, 13, 8); + opts.repeat = arg_get_int_def(ctx, 14, 0); + opts.seed = (uint32_t)arg_get_int_def(ctx, 15, 1); + CLIParserFree(ctx); + + if (mod_len > 0) { + if (strcmp(mod_str, "ask") == 0) { + opts.mod = PM3_MOD_ASK; + } else if (strcmp(mod_str, "fsk") == 0) { + opts.mod = PM3_MOD_FSK; + } else if (strcmp(mod_str, "psk") == 0) { + opts.mod = PM3_MOD_PSK; + } else if (strcmp(mod_str, "nrz") == 0) { + opts.mod = PM3_MOD_NRZ; + } else { + PrintAndLogEx(WARNING, "unknown modulation `%s`", mod_str); + return PM3_EINVARG; + } + } + + if (enc_len > 0) { + if (strcmp(enc_str, "raw") == 0) { + opts.enc = PM3_ENC_RAW; + } else if (strcmp(enc_str, "manchester") == 0) { + opts.enc = PM3_ENC_MANCHESTER; + } else if (strcmp(enc_str, "biphase") == 0) { + opts.enc = PM3_ENC_BIPHASE; + } else { + PrintAndLogEx(WARNING, "unknown encoding `%s`", enc_str); + return PM3_EINVARG; + } + } + + if (len < 256 || len > MAX_GRAPH_TRACE_LEN) { + PrintAndLogEx(WARNING, "len must be between 256 and %d", MAX_GRAPH_TRACE_LEN); + return PM3_EINVARG; + } + if (opts.clk < 4.0 || opts.clk > 1024.0) { + PrintAndLogEx(WARNING, "clk must be between 4 and 1024"); + return PM3_EINVARG; + } + + double *buf = calloc((size_t)len, sizeof(double)); + if (buf == NULL) { + PrintAndLogEx(WARNING, "failed to allocate memory"); + return PM3_EMALLOC; + } + + int res = pm3_fit_generate(&opts, buf, (size_t)len); + if (res != PM3_SUCCESS) { + free(buf); + PrintAndLogEx(WARNING, "failed to generate the signal"); + return res; + } + + double peak = 0.0; + for (int i = 0; i < len; i++) { + if (fabs(buf[i]) > peak) { + peak = fabs(buf[i]); + } + } + if (peak <= 0.0) { + peak = 1.0; + } + + for (int i = 0; i < len; i++) { + g_GraphBuffer[i] = (int32_t)((buf[i] / peak) * 100.0); + } + g_GraphTraceLen = (size_t)len; + free(buf); + + setClockGrid(0, 0); + RepaintGraphWindow(); + + PrintAndLogEx(SUCCESS, "generated " _GREEN_("%d") " samples, %s / %s at clock " _GREEN_("%.2f") + , len + , pm3_mod_name(opts.mod) + , pm3_enc_name(opts.enc) + , opts.clk + ); + return PM3_SUCCESS; +} + + +// Run whichever of the existing demodulators the hypothesis names. This +// command is a parameter suggester, not a decoder - the bit producing path +// stays exactly the one `data rawdemod` uses, so anything it outputs is as +// auditable as before. +static int autodemod_dispatch(const pm3_hyp_t *hyp, int clk, bool invert, bool amp, bool verbose) { + + const int max_err = 100; + + switch (hyp->mod) { + + case PM3_MOD_ASK: + if (hyp->enc == PM3_ENC_BIPHASE) { + return ASKbiphaseDemod(0, clk, invert ? 1 : 0, max_err, verbose); + } + if (hyp->enc == PM3_ENC_MANCHESTER) { + return ASKDemod(clk, invert ? 1 : 0, max_err, 0, amp, verbose, false, 1); + } + // askdemod_ext() halves the clock again for askType 0, so the raw + // path wants the half symbol rate, not the bit rate. See the note + // on detectclock_fit(). + return ASKDemod(clk * 2, invert ? 1 : 0, max_err, 0, amp, verbose, false, 0); + + case PM3_MOD_FSK: + return FSKrawDemod((uint8_t)clk, invert ? 1 : 0, (uint8_t)hyp->fc_hi, (uint8_t)hyp->fc_lo, verbose); + + case PM3_MOD_PSK: + return PSKDemod(clk, invert ? 1 : 0, max_err, verbose); + + case PM3_MOD_NRZ: + return NRZrawDemod(clk, invert ? 1 : 0, max_err, verbose); + } + return PM3_ESOFT; +} + +// Count the chips the demodulator could not call. +static size_t autodemod_markers(void) { + + size_t markers = 0; + for (size_t i = 0; i < g_DemodBufferLen; i++) { + if (g_DemodBuffer[i] > 1) { + markers++; + } + } + return markers; +} + +// Second opinion on an ASK/raw decode, taken at the chip centres. +// +// The shared demodulator recovers its phase from edge positions, which is the +// only option while the clock is unknown. It does mean any asymmetry in the +// waveform lands directly on that phase: an LF tank rings down slower than it +// charges, so on a COTAG capture every falling edge arrives 10 samples late +// and every rising edge 10 samples early against a chip of 384. The chip +// centres are the furthest points from either edge, so once the clock is +// settled they are the better place to decide. +// +// Only ASK/raw, because only there does the demod buffer hold chips at the +// hypothesis clock. The Manchester and biphase paths pair and decode inside +// the shared demodulator, and raw chips are not interchangeable with that. +static bool autodemod_reslice(const pm3_hyp_t *hyp, size_t before) { + + if (hyp->mod != PM3_MOD_ASK || hyp->enc != PM3_ENC_RAW || before == 0) { + return false; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (sig == NULL) { + return false; + } + + pm3_slice_t sl; + const int res = pm3_ask_slice(sig, g_GraphTraceLen, hyp->clk_fine, &sl); + free(sig); + + if (res != PM3_SUCCESS) { + return false; + } + + // Keep whichever demodulation had to guess less often. Both ran at the + // same clock, so the bit counts are comparable and a lower marker count + // cannot be bought by decoding fewer chips. + if (sl.nbits == 0 || sl.nerrors >= before) { + pm3_slice_free(&sl); + return false; + } + + const size_t n = MIN(sl.nbits, (size_t)MAX_DEMOD_BUF_LEN); + setDemodBuff(sl.bits, n, 0); + setClockGrid((uint32_t)(hyp->clk_fine + 0.5), (int)sl.phase); + + PrintAndLogEx(INFO, " resliced...... at the chip centres, %zu demod errors instead of %zu ( eye %.3f )" + , sl.nerrors + , before + , sl.eye + ); + + pm3_slice_free(&sl); + return true; +} + +// The equivalent hand typed command, so the tool teaches rather than replaces +// and the result can go in a script. +static void autodemod_hint(const pm3_hyp_t *hyp, int clk, bool invert, bool amp) { + + const char *flag = "--ar"; + + switch (hyp->mod) { + case PM3_MOD_ASK: + flag = (hyp->enc == PM3_ENC_BIPHASE) ? "--ab" + : (hyp->enc == PM3_ENC_MANCHESTER) ? "--am" : "--ar"; + break; + case PM3_MOD_FSK: + flag = "--fs"; + break; + case PM3_MOD_PSK: + flag = "--p1"; + break; + case PM3_MOD_NRZ: + flag = "--nr"; + break; + } + + const int shown = (hyp->mod == PM3_MOD_ASK && hyp->enc == PM3_ENC_RAW) ? (clk * 2) : clk; + + PrintAndLogEx(HINT, "Hint: the same thing by hand is `" _YELLOW_("data rawdemod %s -c %d%s%s") "`" + , flag + , shown + , invert ? " -i" : "" + , amp ? " -a" : "" + ); +} + +// How clean the demodulation came out. This is about the demodulation, not +// about what the bits mean - putting a format to them is `lf search`'s job and +// this command deliberately stops short of it. +static void autodemod_quality(void) { + + if (g_DemodBufferLen == 0) { + return; + } + + size_t markers = 0; + for (size_t i = 0; i < g_DemodBufferLen; i++) { + if (g_DemodBuffer[i] > 1) { + markers++; + } + } + + if (markers) { + PrintAndLogEx(WARNING, " quality....... %zu of %zu bits are demod errors", markers, g_DemodBufferLen); + } else { + PrintAndLogEx(SUCCESS, " quality....... no demod errors in %zu bits", g_DemodBufferLen); + } +} + +static int CmdAutoDemod(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "data autodemod", + "Tries to work out the modulation, encoding and clock of the wave in the GraphBuffer then run the matching demodulator\n" + "Adds no signal processing of its own, the analysis is `data spectrum` and `data fitscore`\n" + "A fractional clock is resampled onto an integer grid first", + "data autodemod --> analyse and demodulate\n" + "data autodemod --dry-run --> decide and print, demodulate nothing\n" + "data autodemod --thres 6 --> insist on a 6 dB margin before trusting rank 1\n" + "data autodemod --invert --amp --> pass invert and amplify through to the demod\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "start", "", "first sample of the GraphBuffer to use (def 0)"), + arg_int0(NULL, "size", "", "samples to analyse (def 16384)"), + arg_dbl0(NULL, "thres", "", "margin below which rank 1 is called ambiguous (def 3.0)"), + arg_lit0(NULL, "dry-run", "analyse and decide, but do not demodulate"), + arg_lit0("a", "amp", "amplify the signal before ASK demodulation"), + arg_lit0("i", "invert", "invert the demodulated output"), + arg_lit0("v", "verbose", "show the demodulator's own output"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int start = arg_get_int_def(ctx, 1, 0); + int size = arg_get_int_def(ctx, 2, 0); + double thres = arg_get_dbl_def(ctx, 3, 3.0); + bool dry_run = arg_get_lit(ctx, 4); + bool amp = arg_get_lit(ctx, 5); + bool invert = arg_get_lit(ctx, 6); + bool verbose = arg_get_lit(ctx, 7); + CLIParserFree(ctx); + + if (size == 0) { + const size_t avail = (g_GraphTraceLen > (size_t)MAX(start, 0)) ? g_GraphTraceLen - (size_t)MAX(start, 0) : 0; + size = autodemod_window(start, avail); + } + + double *sig = NULL; + size_t count = 0, n = 0; + int res = dsp_prepare(start, size, &sig, &count, &n); + if (res != PM3_SUCCESS) { + return res; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "--- " _CYAN_("Auto demodulate") " -----------------------------------"); + + // the graph buffer is put back the way we found it unless we get bits out + int32_t *backup = NULL; + size_t backup_len = g_GraphTraceLen; + + // 1 - spectral analysis, to prune the bank. Zeroed up front because the + // steps below read it whether or not the analysis had anything to say. + pm3_spec_analysis_t an = {0}; + int mod_mask = 0; + + if (pm3_analyse(sig, count, n, PM3_WIN_HANN, &an) == PM3_SUCCESS) { + + PrintAndLogEx(INFO, " spectrum...... %s, confidence %s" + , pm3_family_name(an.family) + , pm3_confidence_name(an.confidence) + ); + + // only prune on a hint worth trusting, otherwise search everything + if (an.confidence >= PM3_CONF_MEDIUM) { + switch (an.family) { + case PM3_FAM_FSK: + mod_mask = 1 << PM3_MOD_FSK; + break; + case PM3_FAM_PSK: + mod_mask = 1 << PM3_MOD_PSK; + break; + case PM3_FAM_ASK: + case PM3_FAM_MANCHESTER: + mod_mask = (1 << PM3_MOD_ASK) | (1 << PM3_MOD_NRZ); + break; + case PM3_FAM_NRZ: + mod_mask = (1 << PM3_MOD_NRZ) | (1 << PM3_MOD_ASK); + break; + case PM3_FAM_UNKNOWN: + break; + } + } + } + + // 1b - a switched carrier has to be rectified before anything downstream + // will work. The demodulators threshold the samples as they stand, and a + // capture with two samples per carrier period alternates sign on every + // one of them, so every symbol averages to zero and no demodulator can + // ever produce a bit. Replace the buffer with its envelope and carry on. + if (an.needs_envelope) { + + PrintAndLogEx(INFO, " carrier....... rf/%.2f switched on and off ( envelope depth %.2f )" + , an.carrier_clk + , an.envelope_depth + ); + + double *whole = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + double *env = (whole != NULL) ? pm3_envelope(whole, g_GraphTraceLen, 0) : NULL; + free(whole); + + if (env != NULL) { + + autodemod_backup(&backup, backup_len); + autodemod_store(env, g_GraphTraceLen); + free(env); + + double *re = pm3_extract(g_GraphBuffer, g_GraphTraceLen, (size_t)start, count); + if (re != NULL) { + free(sig); + sig = re; + } + + PrintAndLogEx(INFO, " rectified..... the data is in the envelope, demodulating that instead"); + + } else { + PrintAndLogEx(WARNING, " rectified..... failed to allocate, carrying on with the raw samples"); + } + } + + if (mod_mask) { + PrintAndLogEx(INFO, " bank.......... pruned to %s", pm3_family_name(an.family)); + } else { + PrintAndLogEx(INFO, " bank.......... full, the spectral hint was not strong enough to prune on"); + } + + // 2 - rank the hypotheses + pm3_fit_opts_t opts = {0}; + opts.mod_mask = mod_mask; + + pm3_fit_t fit; + res = pm3_fit_run(sig, count, &opts, &fit); + + // a pruned bank that finds nothing is worse than no pruning + if (res != PM3_SUCCESS && mod_mask != 0) { + PrintAndLogEx(INFO, " bank.......... pruned search came up empty, retrying with everything"); + opts.mod_mask = 0; + res = pm3_fit_run(sig, count, &opts, &fit); + } + free(sig); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "no hypothesis fit the signal"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data spectrum --sq") "` to see what is actually there"); + return res; + } + + const pm3_hyp_t *best = &fit.items[0]; + + PrintAndLogEx(SUCCESS, " decision...... " _GREEN_("%s / %s") " at clock " _GREEN_("%.2f") + , pm3_mod_name(best->mod) + , pm3_enc_name(best->enc) + , best->clk_fine + ); + PrintAndLogEx(INFO, " margin........ %.2f dB, SNR %.2f dB, eye %.3f", best->margin, best->snr_dd, best->eye); + + // 3 - an ambiguous choice is reported, not refused. + // + // A structural promotion is not a score race, so a negative margin there + // is expected and says nothing about confidence - the run lengths settled + // it, and they are not a matter of degree. + if (fit.promoted) { + PrintAndLogEx(INFO, " chosen on..... run length evidence, not on score"); + } else if (best->margin < thres) { + PrintAndLogEx(WARNING, "margin %.2f dB is below the %.2f dB threshold, this choice is ambiguous" + , best->margin + , thres + ); + const size_t show = MIN((size_t)3, fit.count); + for (size_t i = 0; i < show; i++) { + PrintAndLogEx(INFO, " %zu. %s / %s at clock %.2f ( SNR %.2f dB )" + , i + 1 + , pm3_mod_name(fit.items[i].mod) + , pm3_enc_name(fit.items[i].enc) + , fit.items[i].clk_fine + , fit.items[i].snr_dd + ); + } + } + + if (dry_run) { + autodemod_hint(best, (int)(best->clk_fine + 0.5), invert, amp); + PrintAndLogEx(INFO, "dry run, nothing was demodulated and the DemodBuffer is untouched"); + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; + } + + // 4 - put a fractional clock onto an integer grid before dispatching. + // + // This is the whole point of the exercise. A clock of 64.19 walks a full + // symbol out of step after 340 symbols, so the demodulator gets the start + // of the capture right and turns the rest to mush - which presents to the + // user as a noisy tag rather than as a wrong clock. + const double frac = fabs(best->clk_fine - floor(best->clk_fine + 0.5)); + + if (frac > 0.05) { + + const double target = floor(best->clk_fine + 0.5); + const double ratio = target / best->clk_fine; + + double *whole = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (whole != NULL) { + + size_t out_len = 0; + double *rs = pm3_resample(whole, g_GraphTraceLen, ratio, &out_len); + free(whole); + + if (rs != NULL) { + + const size_t was = g_GraphTraceLen; + + autodemod_backup(&backup, g_GraphTraceLen); + autodemod_store(rs, out_len); + free(rs); + + PrintAndLogEx(INFO, " resampled..... clock %.2f -> %.0f, buffer %zu -> %zu samples" + , best->clk_fine + , target + , was + , out_len + ); + PrintAndLogEx(INFO, " a fractional clock drifts out of step across a long capture, this fixes that"); + } + } + } + + // 5..7 - dispatch, and fall through to the next ranked hypothesis on failure + const size_t tries = MIN((size_t)3, fit.count); + bool ok = false; + + for (size_t i = 0; i < tries; i++) { + + const pm3_hyp_t *h = &fit.items[i]; + const int clk = (int)(h->clk_fine + 0.5); + + PrintAndLogEx(INFO, " attempt %zu..... %s / %s at clock %d", i + 1, pm3_mod_name(h->mod), pm3_enc_name(h->enc), clk); + + if (autodemod_dispatch(h, clk, invert, amp, verbose) == PM3_SUCCESS && g_DemodBufferLen > 0) { + + autodemod_reslice(h, autodemod_markers()); + + PrintAndLogEx(SUCCESS, " demodulated... " _GREEN_("%zu") " bits into the DemodBuffer", g_DemodBufferLen); + autodemod_quality(); + autodemod_hint(h, clk, invert, amp); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("lf search -1") "` to identify what the bits are"); + ok = true; + break; + } + + PrintAndLogEx(INFO, " no bits, moving on"); + } + + if (ok == false) { + + PrintAndLogEx(WARNING, "none of the top %zu hypotheses produced bits", tries); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("data fitscore --all") "` to see the whole ranking"); + + // leave the user's buffer as we found it if we changed it for nothing + if (backup != NULL) { + memcpy(g_GraphBuffer, backup, backup_len * sizeof(int32_t)); + g_GraphTraceLen = backup_len; + PrintAndLogEx(INFO, "the graph buffer has been put back the way it was"); + } + } + + free(backup); + pm3_fit_free(&fit); + PrintAndLogEx(NORMAL, ""); + return ok ? PM3_SUCCESS : PM3_ESOFT; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("General") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------ " _CYAN_("General") " -------------------------------"}, {"clear", CmdBuffClear, AlwaysAvailable, "Clears various buffers used by the graph window"}, {"hide", CmdHide, AlwaysAvailable, "Hide the graph window"}, {"load", CmdLoad, AlwaysAvailable, "Load contents of file into graph window"}, @@ -4120,7 +5734,7 @@ static command_t CommandTable[] = { {"setdebugmode", CmdSetDebugMode, AlwaysAvailable, "Set Debugging Level on client side"}, {"xor", CmdXor, AlwaysAvailable, "Xor a input string"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Modulation") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "----------------------------- " _CYAN_("Modulation") " -----------------------------"}, {"biphaserawdecode", CmdBiphaseDecodeRaw, AlwaysAvailable, "Biphase decode bin stream in DemodBuffer"}, {"detectclock", CmdDetectClockRate, AlwaysAvailable, "Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer"}, {"fsktonrz", CmdFSKToNRZ, AlwaysAvailable, "Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk)"}, @@ -4128,7 +5742,13 @@ static command_t CommandTable[] = { {"modulation", CmdDataModulationSearch, AlwaysAvailable, "Identify LF signal for clock and modulation"}, {"rawdemod", CmdRawDemod, AlwaysAvailable, "Demodulate the data in the GraphBuffer and output binary"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Graph") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "-------------------------- " _CYAN_("Frequency domain") " --------------------------"}, + {"autodemod", CmdAutoDemod, AlwaysAvailable, "Detect modulation, encoding and clock, then demodulate"}, + {"fft", CmdFFT, AlwaysAvailable, "Fourier transform of the GraphBuffer"}, + {"fitscore", CmdFitScore, AlwaysAvailable, "Rank matched filter hypotheses for modulation, encoding and clock"}, + {"spectrum", CmdSpectrum, AlwaysAvailable, "Spectral peaks, symbol rate and modulation family hint"}, + + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------- " _CYAN_("Graph") " --------------------------------"}, {"askedgedetect", CmdAskEdgeDetect, AlwaysAvailable, "Adjust Graph for manual ASK demod"}, {"autocorr", CmdAutoCorr, AlwaysAvailable, "Autocorrelation over window"}, {"convertbitstream", CmdConvertBitStream, AlwaysAvailable, "Convert GraphBuffer's 0/1 values to 127 / -127"}, @@ -4150,7 +5770,7 @@ static command_t CommandTable[] = { {"undecimate", CmdUndecimate, AlwaysAvailable, "Un-decimate samples"}, {"zerocrossings", CmdZerocrossings, AlwaysAvailable, "Count time between zero-crossings"}, - {"-----------", CmdHelp, AlwaysAvailable, "------------------------- " _CYAN_("Operations") "-------------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "---------------------------- " _CYAN_("Operations") " ------------------------------"}, {"asn1", CmdAsn1Decoder, AlwaysAvailable, "ASN1 decoder"}, {"atr", CmdAtrLookup, AlwaysAvailable, "ATR lookup"}, {"bitsamples", CmdBitsamples, IfPm3Present, "Get raw samples as bitstring"}, @@ -4161,7 +5781,8 @@ static command_t CommandTable[] = { {"samples", CmdSamples, IfPm3Present, "Get raw samples for graph window ( GraphBuffer )"}, {"qrcode", CmdQRcode, AlwaysAvailable, "Create a QR code"}, - {"-----------", CmdHelp, IfClientDebugEnabled, "------------------------- " _CYAN_("Debug") "-------------------------"}, + {"-----------", CmdHelp, IfClientDebugEnabled, "------------------------------- " _CYAN_("Debug") " --------------------------------"}, + {"gensignal", CmdGenSignal, IfClientDebugEnabled, "Generate a synthetic LF waveform into the GraphBuffer"}, {"test_ss8", CmdTestSaveState8, IfClientDebugEnabled, "Test the implementation of Buffer Save States (8-bit buffer)"}, {"test_ss32", CmdTestSaveState32, IfClientDebugEnabled, "Test the implementation of Buffer Save States (32-bit buffer)"}, {"test_ss32s", CmdTestSaveState32S, IfClientDebugEnabled, "Test the implementation of Buffer Save States (32-bit signed buffer)"}, diff --git a/client/src/cmdflashmemspiffs.c b/client/src/cmdflashmemspiffs.c index 170724695..ad27753a4 100644 --- a/client/src/cmdflashmemspiffs.c +++ b/client/src/cmdflashmemspiffs.c @@ -467,7 +467,7 @@ static int CmdFlashMemSpiFFSWipe(const char *Cmd) { CLIParserInit(&ctx, "mem spiffs wipe", _RED_("* * * Warning * * *") " \n" _CYAN_("This command wipes all files on the device SPIFFS file system"), - "mem spiffs wipe"); + "mem spiffs wipe"); void *argtable[] = { arg_param_begin, @@ -512,11 +512,11 @@ static int CmdFlashMemSpiFFSUpload(const char *Cmd) { CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)src, FILE_PATH_SIZE, &slen); int dlen = 0; - char dest[32] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)dest, 32, &dlen); + char dst[32] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 2), (uint8_t *)dst, 32, &dlen); CLIParserFree(ctx); - PrintAndLogEx(DEBUG, "Upload `" _YELLOW_("%s") "` -> `" _YELLOW_("%s") "`", src, dest); + PrintAndLogEx(DEBUG, "Upload `" _YELLOW_("%s") "` -> `" _YELLOW_("%s") "`", src, dst); size_t datalen = 0; uint8_t *data = NULL; @@ -527,11 +527,12 @@ static int CmdFlashMemSpiFFSUpload(const char *Cmd) { return PM3_EFILE; } - res = flashmem_spiffs_load(dest, data, datalen); + res = flashmem_spiffs_load(dst, data, datalen); free(data); - if (res == PM3_SUCCESS) - PrintAndLogEx(SUCCESS, "Wrote "_GREEN_("%zu") " bytes to file "_GREEN_("%s"), datalen, dest); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "Wrote "_GREEN_("%zu") " bytes to file "_GREEN_("%s"), datalen, dst); + } PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("mem spiffs tree") "` to verify"); return res; diff --git a/client/src/cmdhf.c b/client/src/cmdhf.c index eb7ac64dc..b58a7066b 100644 --- a/client/src/cmdhf.c +++ b/client/src/cmdhf.c @@ -395,7 +395,7 @@ int CmdHFTune(const char *Cmd) { PrintAndLogEx(WARNING, "timeout while waiting for Proxmark HF shutdown, aborting"); return PM3_ETIMEOUT; } - PrintAndLogEx(NORMAL, "\x1b%c[2K\r", 30); + PrintAndLogEx(NORMAL, _CLR_LINE_ "\r"); if (verbose) { PrintAndLogEx(INFO, "Min....... %u mV", v_min); diff --git a/client/src/cmdlf.c b/client/src/cmdlf.c index 4c3372f39..56d6e71eb 100644 --- a/client/src/cmdlf.c +++ b/client/src/cmdlf.c @@ -49,6 +49,7 @@ #include "cmdlfidteck.h" // for idteck menu #include "cmdlfio.h" // for ioprox menu #include "cmdlfcotag.h" // for COTAG menu +#include "pm3_dsp.h" // pm3_extract, pm3_is_switched_carrier #include "cmdlfdestron.h" // for FDX-A FECAVA Destron menu #include "cmdlffdxb.h" // for FDX-B menu #include "cmdlfgallagher.h" // for GALLAGHER menu @@ -68,6 +69,7 @@ #include "cmdlfsecurakey.h" // for securakey menu #include "cmdlft55xx.h" // for t55xx menu #include "cmdlfti.h" // for ti menu +#include "cmdlftrovan.h" // for trovan menu #include "cmdlfviking.h" // for viking menu #include "cmdlfvisa2000.h" // for VISA2000 menu #include "cmdlfzx8211.h" // for ZX8211 menu @@ -239,7 +241,7 @@ static int CmdLFTune(const char *Cmd) { return PM3_ETIMEOUT; } - PrintAndLogEx(NORMAL, "\x1b%c[2K\r", 30); + PrintAndLogEx(NORMAL, _CLR_LINE_ "\r"); if (verbose) { PrintAndLogEx(INFO, "Min....... %u mV", v_min); PrintAndLogEx(INFO, "Max....... %u mV", v_max); @@ -1834,11 +1836,11 @@ int CmdLFRelay(const char *Cmd) { " --rdr : Reading device, act as IP client and reads LF tag and sends data\n" " --tag : Simulation device, act as IP server and simulates relayed data\n", _WHITE_("Device A, reading LF tag, client") "\n" - "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" - "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" + "lf relay --rdr --ip 192.168.1.141 -> Client, connect to IP 192.168.1.141:8000\n" + "lf relay --rdr --ip 192.168.1.141 -p 18111 -> Client, connect to IP 192.168.1.141:18111 \n\n" _WHITE_("Device B, simulate LF tag, server") "\n" - "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" - "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" + "lf relay --tag -p 8111 -> Server listening port 8111, recv 40000 samples\n" + "lf relay --tag -s 10000 -> Server listening port 8000, recv 10000 samples\n" ); void *argtable[] = { @@ -2211,6 +2213,35 @@ int CmdLFfind(const char *Cmd) { } } */ + if (demodTrovan(false) == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "\nValid " _GREEN_("Trovan ID") " found!"); + if (search_cont) { + found++; + } else { + goto out; + } + } + + // COTAG last, and only when the capture actually looks like one. + if (found == 0) { + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, g_GraphTraceLen); + if (sig != NULL) { + + const bool switched = pm3_is_switched_carrier(sig, g_GraphTraceLen); + free(sig); + + if (switched && demodCOTAG(false, -1, -1) == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "\nValid " _GREEN_("COTAG ID") " found!"); + if (search_cont) { + found++; + } else { + goto out; + } + } + } + } + if (found == 0) { PrintAndLogEx(FAILED, _RED_("No known 125/134 kHz tags found!")); } @@ -2351,6 +2382,7 @@ static command_t CommandTable[] = { {"securakey", CmdLFSecurakey, AlwaysAvailable, "{ Securakey RFIDs... }"}, {"ti", CmdLFTI, AlwaysAvailable, "{ TI CHIPs... }"}, {"t55xx", CmdLFT55XX, AlwaysAvailable, "{ T55xx CHIPs... }"}, + {"trovan", CmdLFTrovan, AlwaysAvailable, "{ Trovan animal IDs... }"}, {"viking", CmdLFViking, AlwaysAvailable, "{ Viking RFIDs... }"}, {"visa2000", CmdLFVisa2k, AlwaysAvailable, "{ Visa2000 RFIDs... }"}, // {"zx", CmdLFZx8211, AlwaysAvailable, "{ ZX8211 RFIDs... }"}, diff --git a/client/src/cmdlfcotag.c b/client/src/cmdlfcotag.c index 6bf01b82b..759d6ac09 100644 --- a/client/src/cmdlfcotag.c +++ b/client/src/cmdlfcotag.c @@ -54,75 +54,6 @@ static void find_avg_high_low(const int32_t *samples, int num_samples, double *out_high_level, double *out_low_level); static double trimmed_mean_abs(const int32_t *samples, int start, int count); -#if 0 -// TODO: With the new cotag implementation shall we remove this old version -// or is there anything we could use from it? -// COTAG demod should be able to use g_GraphBuffer, -// when data load samples -int demodCOTAG(bool verbose) { - (void) verbose; // unused so far - - uint8_t bits[COTAG_BITS] = {0}; - size_t bitlen = COTAG_BITS; - memcpy(bits, g_DemodBuffer, COTAG_BITS); - - uint8_t inv_bits[COTAG_BITS] = {0}; - memcpy(inv_bits, g_DemodBuffer, COTAG_BITS); - - uint8_t alignPos = 0; - uint16_t err = manrawdecode(bits, &bitlen, 1, &alignPos); - if (err > 50) { - PrintAndLogEx(DEBUG, "DEBUG: Error - COTAG too many errors: %d", err); - return PM3_ESOFT; - } - - setDemodBuff(bits, bitlen, 0); - - //got a good demod - uint16_t cn = bytebits_to_byteLSBF(bits + 1, 16); - uint32_t fc = bytebits_to_byteLSBF(bits + 1 + 16, 8); - - uint32_t raw1 = bytebits_to_byteLSBF(bits, 32); - uint32_t raw2 = bytebits_to_byteLSBF(bits + 32, 32); - uint32_t raw3 = bytebits_to_byteLSBF(bits + 64, 32); - uint32_t raw4 = bytebits_to_byteLSBF(bits + 96, 32); - - - /* - fc 161: 1010 0001 -> LSB 1000 0101 - cn 33593 1000 0011 0011 1001 -> LSB 1001 1100 1100 0001 - cccc cccc cccc cccc ffffffff - 0 1001 1100 1100 0001 1000 0101 0000 0000 100001010000000001111011100000011010000010000000000000000000000000000000000000000000000000000000100111001100000110000101000 - 1001 1100 1100 0001 10000101 - - COTAG FC/272 - 1 7 7 D E 2 0 0 8 0 0 0 3 9 2 0 D 0 4 0000000000000 - 0001 0111 0111 1101 1110 0010 0000 0000 1000 0000 0000 0000 0011 1001 0010 0000 1101 0000 0100 0000000000000000000000000000000000000000000000000000000 - 0001 0111 0111 1101 1110 001 0010 1001 0011 1000 0110 0100 - - */ - PrintAndLogEx(SUCCESS, "COTAG Found: FC " _GREEN_("%u")", CN: " _GREEN_("%u")" Raw: %08X%08X%08X%08X", fc, cn, raw1, raw2, raw3, raw4); - - bitlen = COTAG_BITS; - err = manrawdecode(inv_bits, &bitlen, 0, &alignPos); - if (err < 50) { - uint32_t cn_large = bytebits_to_byte(inv_bits + 1, 23); - cn_large = reflect32(cn_large) >> 9; - uint8_t a = bytebits_to_byte(inv_bits + 48, 4); - uint8_t b = bytebits_to_byte(inv_bits + 52, 4); - uint8_t c = bytebits_to_byte(inv_bits + 56, 4); - uint16_t fc_large = NIBBLE_LOW(c) << 8 | NIBBLE_LOW(b) << 4 | NIBBLE_LOW(a); - - raw1 = bytebits_to_byte(inv_bits, 32); - raw2 = bytebits_to_byte(inv_bits + 32, 32); - raw3 = bytebits_to_byte(inv_bits + 64, 32); - raw4 = bytebits_to_byte(inv_bits + 96, 32); - PrintAndLogEx(SUCCESS, " FC " _GREEN_("%u")", CN: " _GREEN_("%u")" Raw: %08X%08X%08X%08X", fc_large, cn_large, raw1, raw2, raw3, raw4); - } - return PM3_SUCCESS; -} -#endif - /** * Demodulate COTAG samples. * @@ -141,64 +72,80 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s uint8_t *manchester_demod = NULL; uint8_t *manchester_demod_reversed = NULL; - /* Calculate min, max, average */ + int manchester_count = 0; + const char *fail = NULL; + int64_t sum = 0; min = max = samples[0]; + for (int i = 0; i < num_samples; i++) { sum += samples[i]; - if (samples[i] < min) min = samples[i]; - if (samples[i] > max) max = samples[i]; + if (samples[i] < min) { + min = samples[i]; + } + + if (samples[i] > max) { + max = samples[i]; + } } + avg = (double)sum / (double)num_samples; if (verbose) { PrintAndLogEx(INFO, " Clock: %d", clock); - if (threshold < 0) + if (threshold < 0) { PrintAndLogEx(INFO, " Threshold: auto"); - else + } else { PrintAndLogEx(INFO, " Threshold: %.2f", threshold); + } PrintAndLogEx(INFO, " Min : %" PRId32, min); PrintAndLogEx(INFO, " Max : %" PRId32, max); - PrintAndLogEx(INFO, " Avg : %.2f\n", avg); - printf("\n"); + PrintAndLogEx(INFO, " Avg : %.2f", avg); + PrintAndLogEx(NORMAL, ""); } - /* DC offset removal: subtract average from every sample */ + // DC offset removal: subtract average from every sample for (int i = 0; i < num_samples; i++) { double v = round((double)samples[i] - avg); samples[i] = (int32_t)v; } - /* Auto threshold estimation: */ + // Auto threshold estimation if (threshold < 0) { double high_level, low_level; find_avg_high_low(samples, num_samples, clock, &high_level, &low_level); threshold = (low_level + high_level) * 0.5; - if (verbose) - PrintAndLogEx(INFO, " Auto threshold: low_level=%.2f, high_level=%.2f --> threshold=%.2f", low_level, high_level, threshold); + if (verbose) { + PrintAndLogEx(INFO, " Auto threshold: low_level = %.2f, high_level = %.2f --> threshold = %.2f", low_level, high_level, threshold); + } } - /* Auto clock-start detection (first edge detection) */ + // Auto clock-start detection (first edge detection) if (clock_start == -1) { clock_start = detect_edge(samples, num_samples, 0, threshold); - if (verbose) + if (verbose) { PrintAndLogEx(INFO, " Detected clock start candidate: sample #%d", clock_start); + } } - /* High/low raw demodulation of clock-half cycles */ + // High/low raw demodulation of clock-half cycles int high_low_demod_01_len = (num_samples - clock_start) / clock_half + 16; + high_low_demod_01 = calloc(high_low_demod_01_len, sizeof(uint8_t)); - if (!high_low_demod_01) { + if (high_low_demod_01 == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } + int high_low_demod_01_count = 0; for (int idx = clock_start; idx + clock <= num_samples; idx += clock) { - /* Trimmed mean of absolute values of first half: */ + + // Trimmed mean of absolute values of first half double clock_half1_val = trimmed_mean_abs(samples, idx, clock_half); - /* Trimmed mean of absolute values of second half: */ + + // Trimmed mean of absolute values of second half double clock_half2_val = trimmed_mean_abs(samples, idx + clock_half, clock_half); uint8_t half1 = (clock_half1_val >= threshold) ? 1 : 0; @@ -209,38 +156,52 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s high_low_demod_01_count += 2; } - /* Manchester demodulation buffer: */ + // Manchester demodulation buffer int manchester_demod_len = high_low_demod_01_count / 2; manchester_demod = calloc(manchester_demod_len, sizeof(uint8_t)); - if (!manchester_demod) { + if (manchester_demod == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } - /* Manchester demodulation (Thomas) from raw high/low demod (high_low_demod_01) */ + // Manchester demodulation (Thomas) from raw high/low demod (high_low_demod_01) const int MAX_MANDEMOD_ERRORS = 64; bool demod_success = true; - int manchester_count = 0; int mandemod_err_count = 0; + for (int i = 0; i + 1 < high_low_demod_01_count;) { + uint8_t half1 = high_low_demod_01[i]; uint8_t half2 = high_low_demod_01[i + 1]; if (half1 == 0 && half2 == 1) { + manchester_demod[manchester_count++] = 0; i += 2; + } else if (half1 == 1 && half2 == 0) { + manchester_demod[manchester_count++] = 1; i += 2; + } else { - if (verbose) - PrintAndLogEx(INFO, " Manchester demod error: index %d (sample #%" PRId32 "): half1=%u, half2=%u --> clock align by half cycle", - i, (int32_t)(clock_start + i * clock_half), (unsigned)half1, (unsigned)half2); - i += 1; /* re-align by one half-clock forward */ + + if (verbose) { + PrintAndLogEx(INFO, " Manchester demod error: index %d (sample #%" PRId32 "): half1=%u, half2=%u --> clock align by half cycle" + , i + , (int32_t)(clock_start + i * clock_half) + , (unsigned)half1, (unsigned)half2 + ); + } + + // re-align by one half-clock forward + i += 1; + mandemod_err_count++; + if (mandemod_err_count >= MAX_MANDEMOD_ERRORS) { - PrintAndLogEx(ERR, " Manchester demod: too many errors (%d), giving up", mandemod_err_count); + fail = "too many Manchester errors"; demod_success = false; break; } @@ -248,31 +209,34 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s } if (demod_success) { - if (verbose) + if (verbose) { PrintAndLogEx(INFO, " Manchester demod: %d bits", manchester_count); - } else + } + } else { goto end; - printf("\n"); + } - /* Reverse order of bits */ + // Reverse order of bits manchester_demod_reversed = calloc(manchester_demod_len, sizeof(uint8_t)); - if (!manchester_demod_reversed) { + if (manchester_demod_reversed == NULL) { PrintAndLogEx(ERR, "Error: out of memory"); rv = PM3_EMALLOC; goto end; } - for (int i = 0; i < manchester_count; i++) - manchester_demod_reversed[i] = manchester_demod[manchester_count - 1 - i]; - { + for (int i = 0; i < manchester_count; i++) { + manchester_demod_reversed[i] = manchester_demod[manchester_count - 1 - i]; + } + + if (verbose) { char manchester_demod_reversed_str[manchester_count + 1]; - for (int i = 0; i < manchester_count; i++) + for (int i = 0; i < manchester_count; i++) { manchester_demod_reversed_str[i] = '0' + manchester_demod_reversed[i]; + } manchester_demod_reversed_str[manchester_count] = '\0'; - PrintAndLogEx(SUCCESS, " Manchester demod reversed:"); - PrintAndLogEx(SUCCESS, " %s", manchester_demod_reversed_str); - printf("\n"); - printf("\n"); + PrintAndLogEx(INFO, " Manchester demod reversed:"); + PrintAndLogEx(INFO, " %s", manchester_demod_reversed_str); + PrintAndLogEx(NORMAL, ""); } /* @@ -284,21 +248,21 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s * Card number: 0x24A88F */ - /* Find preamble. */ + // Find preamble. static const uint8_t preamble_a[] = { - /* type A: 62 zeros followed by 1,0,1,0,0,0 */ + // type A: 62 zeros followed by 1,0,1,0,0,0 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0 }; static const uint8_t preamble_p[] = { - /* type P: 55 zeros followed by 1,0,0,0,0,0,1 */ + // type P: 55 zeros followed by 1,0,0,0,0,0,1 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1 }; static const uint8_t preamble_p0[] = { - /* type P-0: 61 zeros followed by 1 */ + // type P-0: 61 zeros followed by 1 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 @@ -320,8 +284,8 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s for (int i = 0; i < manchester_count && preamble_index < 0; i++) { for (int p = 0; preamble_patterns[p].pat != NULL; p++) { - if (i + preamble_patterns[p].len <= manchester_count && - memcmp(&manchester_demod_reversed[i], preamble_patterns[p].pat, preamble_patterns[p].len) == 0) { + if ( ((i + preamble_patterns[p].len) <= manchester_count) + && (memcmp(&manchester_demod_reversed[i], preamble_patterns[p].pat, preamble_patterns[p].len) == 0)) { preamble_index = i; preamble_type = preamble_patterns[p].name; break; @@ -330,51 +294,64 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s } if (preamble_index < 0) { - PrintAndLogEx(INFO, " Preamble not found in manchester_demod_reversed"); + fail = "no preamble found"; goto end; } - PrintAndLogEx(SUCCESS, " Preamble found (type %s) in manchester_demod_reversed at index %d", - preamble_type, preamble_index); + if (verbose) { + PrintAndLogEx(INFO, " Preamble found (type %s) in manchester_demod_reversed at index %d", preamble_type, preamble_index); + } - /* data_bits: 128 bits starting at preamble */ + // data_bits: 128 bits starting at preamble if (preamble_index + LF_COTAG_DATA_LEN > manchester_count) { - PrintAndLogEx(INFO, " Not enough bits after preamble for full 128-bit data block"); + fail = "preamble found but the capture ends before a full 128 bit block"; rv = PM3_EPARTIAL; goto end; } const uint8_t *data_bits = &manchester_demod_reversed[preamble_index]; - /* Print raw 128 bits */ - { + // Print raw 128 bits + if (verbose) { char str[LF_COTAG_DATA_LEN + 1]; - for (int i = 0; i < LF_COTAG_DATA_LEN; i++) + for (int i = 0; i < LF_COTAG_DATA_LEN; i++) { str[i] = '0' + data_bits[i]; + } str[LF_COTAG_DATA_LEN] = '\0'; PrintAndLogEx(SUCCESS, " data bits: %s", str); } - /* Print bits grouped by 4, space-separated */ - { + + // Print bits grouped by 4, space-separated + if (verbose) { char str[LF_COTAG_DATA_LEN + LF_COTAG_DATA_LEN / 4]; + int p = 0; for (int i = 0; i < LF_COTAG_DATA_LEN; i += 4) { - if (i > 0) str[p++] = ' '; - for (int j = 0; j < 4; j++) + + if (i > 0) { + str[p++] = ' '; + } + + for (int j = 0; j < 4; j++) { str[p++] = '0' + data_bits[i + j]; + } } + str[p] = '\0'; PrintAndLogEx(SUCCESS, " data bits: %s", str); } - /* Print bits as hex nibbles */ - { + + // Print bits as hex nibbles + if (verbose) { char str[LF_COTAG_DATA_LEN / 4 * 5 + 1]; int p = 0; for (int i = 0; i < LF_COTAG_DATA_LEN; i += 4) { - int nibble = (data_bits[i] << 3) - | (data_bits[i + 1] << 2) - | (data_bits[i + 2] << 1) - | data_bits[i + 3]; + + int nibble = (data_bits[i] << 3) + | (data_bits[i + 1] << 2) + | (data_bits[i + 2] << 1) + | data_bits[i + 3]; + str[p++] = ' '; str[p++] = ' '; str[p++] = ' '; @@ -385,17 +362,19 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s PrintAndLogEx(SUCCESS, " data hex: %s", str); } - /* Card number: last 24 bits of data_bits as an integer */ + // Card number: last 24 bits of data_bits as an integer uint32_t c_num = 0; - for (int i = LF_COTAG_DATA_LEN - 24; i < LF_COTAG_DATA_LEN; i++) + for (int i = LF_COTAG_DATA_LEN - 24; i < LF_COTAG_DATA_LEN; i++) { c_num = (c_num << 1) | data_bits[i]; - PrintAndLogEx(SUCCESS, " card number: 0x%X == %u", c_num, c_num); + } - /* Count how many subsequent 128-bit blocks equal data_bits */ + // Count how many subsequent 128-bit blocks equal data_bits int repeat_count = 0; bool fully_repeats = true; int pos = preamble_index + LF_COTAG_DATA_LEN; + while (pos + LF_COTAG_DATA_LEN <= manchester_count) { + if (memcmp(&manchester_demod_reversed[pos], data_bits, LF_COTAG_DATA_LEN) == 0) { repeat_count++; } else { @@ -405,14 +384,28 @@ static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_s pos += LF_COTAG_DATA_LEN; } - if (fully_repeats && repeat_count > 0) - PrintAndLogEx(INFO, " Sequence fully repeats until the end %d time(s)", repeat_count); - else - PrintAndLogEx(INFO, " Sequence does NOT match at index %d (repeat count = %d)", pos, repeat_count); - printf("\n"); + if (verbose) { + if (fully_repeats && repeat_count > 0) { + PrintAndLogEx(INFO, " Sequence fully repeats until the end %d time(s)", repeat_count); + } else { + PrintAndLogEx(INFO, " Sequence does NOT match at index %d (repeat count = %d)", pos, repeat_count); + } + } + + PrintAndLogEx(SUCCESS, "COTAG - Card number " _GREEN_("%u") " ( 0x%06X )", c_num, c_num); rv = PM3_SUCCESS; end: + // One line when there is no card number to show, so a failed demod says + // how far it got rather than either going silent or dumping every stage. + if (rv != PM3_SUCCESS && fail != NULL) { + PrintAndLogEx(FAILED, "COTAG demod failed - %s ( %d Manchester bits at rf/%d )" + , fail + , manchester_count + , clock + ); + } + free(manchester_demod_reversed); free(manchester_demod); free(high_low_demod_01); @@ -431,7 +424,8 @@ int demodCOTAG(bool verbose, int clock, int threshold) { static int CmdCOTAGDemod(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "lf cotag demod", - "Demodulate COTAG samples from g_GraphBuffer. Try to find COTAG preamble, if found decode / descramble data.", + "Demodulate COTAG samples from g_GraphBuffer.\n" + "Try to find COTAG preamble, if found decode / descramble data.", "lf cotag demod" ); @@ -490,8 +484,9 @@ static int CmdCOTAGReader(const char *Cmd) { return PM3_EINVARG; } - if (g_session.pm3_present == false) + if (g_session.pm3_present == false) { return PM3_ENOTTY; + } uint8_t effective_divisor = (divisor > -1) ? (uint8_t)divisor : LF_COTAG_DIVISOR; @@ -542,97 +537,13 @@ static int CmdCOTAGReader(const char *Cmd) { return ret; } -#if 0 // TODO: Remove this implementation? -// When reading a COTAG. -// 0 = HIGH/LOW signal - maxlength bigbuff -// 1 = translation for HI/LO into bytes with manchester 0,1 - length 300 -// 2 = raw signal - maxlength bigbuff -int CmdCOTAGReader_old(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "lf cotag reader", - "read a COTAG tag, the current support for COTAG is limited. ", - "lf cotag reader -2" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0("1", NULL, "HIGH/LOW signal; maxlength bigbuff"), - arg_lit0("2", NULL, "translation of HIGH/LOW into bytes with manchester 0,1"), - arg_lit0("3", NULL, "raw signal; maxlength bigbuff"), - arg_param_end - }; - - CLIExecWithReturn(ctx, Cmd, argtable, false); - bool mode0 = arg_get_lit(ctx, 1); - bool mode1 = arg_get_lit(ctx, 2); - bool mode2 = arg_get_lit(ctx, 3); - CLIParserFree(ctx); - - if ((mode0 + mode1 + mode2) > 1) { - PrintAndLogEx(ERR, "You can only use one option at a time"); - return PM3_EINVARG; - } - uint8_t mode = 0xFF; - if (mode0) - mode = 0; - if (mode1) - mode = 1; - if (mode2) - mode = 2; - - struct p { - uint8_t mode; - } PACKED payload; - payload.mode = mode; - - PacketResponseNG resp; - clearCommandBuffer(); - SendCommandNG(CMD_LF_COTAG_READ, (uint8_t *)&payload, sizeof(payload)); - - uint8_t timeout = 3; - int res = PM3_SUCCESS; - while (WaitForResponseTimeout(CMD_LF_COTAG_READ, &resp, 1000) == false) { - timeout--; - if (timeout == 0) { - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(WARNING, "command execution time out"); - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - res = PM3_ETIMEOUT; - } - } - - if (res != PM3_SUCCESS) { - return res; - } - - if (timeout != 3) - PrintAndLogEx(NORMAL, ""); - - switch (payload.mode) { - case 0: - case 2: { - CmdPlot(""); - CmdGrid("-x 384"); - getSamples(0, false); - break; - } - case 1: { - memcpy(g_DemodBuffer, resp.data.asBytes, resp.length); - g_DemodBufferLen = resp.length; - return demodCOTAG(true); - } - } - return PM3_SUCCESS; -} -#endif - static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"demod", CmdCOTAGDemod, AlwaysAvailable, "demodulate a COTAG tag"}, {"reader", CmdCOTAGReader, IfPm3Lf, "attempt to read and extract tag data"}, {NULL, NULL, NULL, NULL} }; + static int CmdHelp(const char *Cmd) { (void)Cmd; // Cmd is not used so far CmdsHelp(CommandTable); @@ -662,22 +573,25 @@ static int cmp_int32_asc(const void *a, const void *b) { * resolution for very low amplitude captures. */ static double trimmed_mean_abs(const int32_t *samples, int start, int count) { - /* The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging */ + /// The values are sorted and the top 1/TRIM_DROP_DEN are discarded before averaging const int TRIM_DROP_DEN = 4; // trimmed mean denominator int32_t buf[count]; - for (int k = 0; k < count; k++) + for (int k = 0; k < count; k++) { buf[k] = abs(samples[start + k]); + } qsort(buf, count, sizeof(int32_t), cmp_int32_asc); int keep = count - count / TRIM_DROP_DEN; - if (keep < 1) + if (keep < 1) { keep = 1; + } int64_t sum = 0; - for (int k = 0; k < keep; k++) + for (int k = 0; k < keep; k++) { sum += buf[k]; + } return (double)sum / (double)keep; } @@ -691,24 +605,26 @@ static double trimmed_mean_abs(const int32_t *samples, int start, int count) { * @param out_high_level the highest window trimmed mean seen * @param out_low_level the lowest window trimmed mean seen */ -static void find_avg_high_low(const int32_t *samples, int num_samples, - int clock, - double *out_high_level, double *out_low_level) { +static void find_avg_high_low(const int32_t *samples, int num_samples, int clock, double *out_high_level, double *out_low_level) { const int WINDOW = 256; double high_level = -DBL_MAX; double low_level = DBL_MAX; int scan_end = 8 * clock; - if (scan_end > num_samples) + if (scan_end > num_samples) { scan_end = num_samples; + } for (int i = 0; i + WINDOW <= scan_end; i++) { double window_val = trimmed_mean_abs(samples, i, WINDOW); - if (window_val < low_level) + if (window_val < low_level) { low_level = window_val; - if (window_val > high_level) + } + + if (window_val > high_level) { high_level = window_val; + } } *out_high_level = high_level; @@ -726,12 +642,13 @@ static void find_avg_high_low(const int32_t *samples, int num_samples, * * @return Index of the detected edge, or 0 if none found. */ -static int detect_edge(const int32_t *samples, int num_samples, - int index_start, double threshold) { +static int detect_edge(const int32_t *samples, int num_samples, int index_start, double threshold) { + const int GLITCH_WINDOW = 10; - if (num_samples <= 0 || index_start < 0 || index_start >= num_samples) + if (num_samples <= 0 || index_start < 0 || index_start >= num_samples) { return 0; + } bool prev_high = abs(samples[index_start]) >= threshold; @@ -739,18 +656,18 @@ static int detect_edge(const int32_t *samples, int num_samples, bool curr_high = abs(samples[i]) >= threshold; if (curr_high != prev_high) { - /* Need enough samples on both sides for glitch check */ + // Need enough samples on both sides for glitch check if ((int)i < GLITCH_WINDOW || i + GLITCH_WINDOW > num_samples) { prev_high = curr_high; continue; } - /* Sum of GLITCH_WINDOW absolute values before the crossing */ + // Sum of GLITCH_WINDOW absolute values before the crossing int64_t before_sum = 0; for (int k = 0; k < GLITCH_WINDOW; k++) before_sum += abs(samples[i - GLITCH_WINDOW + k]); - /* Sum of GLITCH_WINDOW absolute values after the crossing */ + // Sum of GLITCH_WINDOW absolute values after the crossing int64_t after_sum = 0; for (int k = 0; k < GLITCH_WINDOW; k++) after_sum += abs(samples[i + k]); @@ -758,8 +675,9 @@ static int detect_edge(const int32_t *samples, int num_samples, bool before_high = (double)before_sum >= threshold * GLITCH_WINDOW; bool after_high = (double)after_sum >= threshold * GLITCH_WINDOW; - if (before_high != after_high) + if (before_high != after_high) { return (int)i; + } } prev_high = curr_high; diff --git a/client/src/cmdlfem4x70.c b/client/src/cmdlfem4x70.c index 8fd80e3d7..a1b0c5b32 100644 --- a/client/src/cmdlfem4x70.c +++ b/client/src/cmdlfem4x70.c @@ -310,7 +310,6 @@ static int brute_em4x70(const em4x70_cmd_input_brute_t *opts, em4x70_cmd_output_ // Lowers the cognitive load AND makes it easier to understand. // opts structure stored value in BIG ENDIAN // Note that the FIRMWARE side will swap the byte order back to BIG ENDIAN. - // (yes, this is a bit of a mess, but it is what it is for now...) uint16_t start_key_be = (opts->partial_key_start[0] << 8) | opts->partial_key_start[1]; etd.start_key = start_key_be; @@ -994,7 +993,7 @@ static int CmdEM4x70Recover(const char *Cmd) { alt_grn.grn[2] ); } - printf("\n"); + PrintAndLogEx(NORMAL, ""); // which of those keys actually validates? if (recover_ctx.opts.verify) { diff --git a/client/src/cmdlfem4x70.h b/client/src/cmdlfem4x70.h index 7b5afdae8..18f066155 100644 --- a/client/src/cmdlfem4x70.h +++ b/client/src/cmdlfem4x70.h @@ -20,12 +20,12 @@ #define CMDLFEM4X70_H__ #include "common.h" +#include #define TIMEOUT 2000 int CmdLFEM4X70(const char *Cmd); -// for `lf search`: bool detect_4x70_block(void); #endif diff --git a/client/src/cmdlft55xx.c b/client/src/cmdlft55xx.c index b07c4e015..728d296b5 100644 --- a/client/src/cmdlft55xx.c +++ b/client/src/cmdlft55xx.c @@ -22,6 +22,8 @@ #endif #include "cmdlft55xx.h" +#include "pm3_dsp.h" // pm3_extract +#include "pm3_fit.h" // matched filter hypothesis bank #include #include // MingW #include "cmdparser.h" // command_t @@ -51,7 +53,11 @@ #define T55XX_PrintConfig true #define T55XX_DontPrintConfig false -//static uint8_t bit_rates[9] = {8, 16, 32, 40, 50, 64, 100, 128, 0}; +#define T55XX_PSK3_MAX_CAND 32 + +static size_t t55xx_psk3_block0_candidates(uint32_t observed, uint8_t clk, uint32_t *out, size_t max); +static bool t55xx_config_psk3_ambiguous(void); +static bool t55xx_psk3_probe(bool usepwd, uint32_t password, uint8_t downlink_mode); // Default configuration static t55xx_conf_block_t config = { @@ -66,265 +72,6 @@ static t55xx_conf_block_t config = { }; static t55xx_memory_item_t cardmem[T55x7_BLOCK_COUNT] = {{0}}; -/* -#define DC(x) ((x) + 128) - -static bool t55xx_is_valid_block0(uint32_t block, uint8_t rfclk, uint8_t pskcf) { - - if (block == 0x00) { - return false; - } - - // Master key = 6 or 9 - if ((((block >> 28)& 0xF) != 0x0) && - (((block >> 28)& 0xF) != 0x6) && - (((block >> 28)& 0xF) != 0x9)) { - return false; - } - - // X Mode - if ( ((block >> 17) & 1) && ((((block >> 28) & 0xf) == 0x6) || (((block >> 28) & 0xf) == 0x9)) ) { - // X mode fixed 0 bits - if ((block & 0x0F000000) != 0x00) { - return false; - } - } else { - // / Basic Mode fixed 0 bits - if ((block & 0x0FE00106) != 0x00) { - return false; - } - } - - // Modulation - if ( (((block >> 12) & 0x1F) != 0x00) && // Direct - (((block >> 12) & 0x1F) != 0x01) && // PSK1 - (((block >> 12) & 0x1F) != 0x02) && // PSK2 - (((block >> 12) & 0x1F) != 0x03) && // PSK3 - (((block >> 12) & 0x1F) != 0x04) && // FSK1 - (((block >> 12) & 0x1F) != 0x05) && // FSK2 - (((block >> 12) & 0x1F) != 0x06) && // FSK1a - (((block >> 12) & 0x1F) != 0x07) && // FSK2a - (((block >> 12) & 0x1F) != 0x08) && // Manchester - (((block >> 12) & 0x1F) != 0x10) && // Bi-phase - (((block >> 12) & 0x1F) != 0x18) ) { // Reserved - return false; - } - - PrintAndLogEx(DEBUG, "suggested block... %08x", block); - - // check pskcf - if ((pskcf <= 3) && (((block >> 10) & 0x3) != pskcf)) { - PrintAndLogEx(DEBUG, "fail 6 - %u %u", pskcf, (block >> 10) & 0x3); - return false; - } - - uint8_t testSpeed; - - // check rfclk - if ((((block >> 17) & 1) == 1) && ((((block >> 28) & 0xf) == 0x6) || (((block >> 28) & 0xf) == 0x9)) ){ // X mode speedBits - testSpeed = (((block >> 18) & 0x3F) * 2) + 2; - } else { - uint8_t basicSpeeds[] = {8,16,32,40,50,64,100,128}; - testSpeed = basicSpeeds[(block >> 18) & 0x7]; - } - - if (testSpeed != rfclk) { - PrintAndLogEx(DEBUG, "fail 7 - %u %u ", testSpeed , rfclk); - return false; - } - return true; -} - -static void t55xx_psk1_demod (int *data, uint8_t rfclk, uint8_t pskcf, uint32_t *block) { - - if ((rfclk < 8) || (rfclk > 128)) { - return; - } - - switch (pskcf) { - case 0: { - pskcf = 2; - break; - } - case 1: { - pskcf = 4; - break; - } - case 2: { - pskcf = 8; - break; - } - default: { - break; - } - } - - int startOffset = 1; // where to start reading data samples - int sampleCount = 0; // Counter for 1 bit of samples - int samples0, samples1; // Number of High even and odd bits in a sample. - int startBitOffset = 1; // which bit to start at e.g. for rf/32 1 33 65 ... - int bitCount = 0; - uint32_t myblock = 0; - int offset; - uint8_t drift = 0; - uint8_t tuneOffset = 0; - - drift = (rfclk % pskcf); // 50 2 = 1 50 4 = 2 - - // locate first "0" - high transisiton for correct start offset - while (DC(data[startOffset]) <= (DC(data[startOffset - 1]) + 5)) { - // sampleToggle ^= 1; - startOffset++; - } - - // Start sample may be 1 off due to sample alignment with chip modulation - // so seach for the first lower value, and adjust as needed - if (pskcf == 2) { - - tuneOffset = startOffset + 1; - - while (DC(data[tuneOffset]) >= (DC(data[tuneOffset - 1]) + 5)) { - tuneOffset++; - } - - if ((tuneOffset - startOffset - 1) % 2) { - startOffset++; - } - } - - uint8_t pskcfidx = 0; - - // Get the offset to the first sample of the data block - offset = (rfclk * startBitOffset) + startOffset; - - pskcfidx = (drift / 2); - pskcfidx = pskcfidx % pskcf; - - // while data my be in the settle period of sampling - // First 18 - 24 bits not usable for reference only - while (offset < 20) { - offset += (32 * rfclk); - } - - // Read 1 block of data - for (bitCount = 0; bitCount < 32; bitCount++) { - - samples0 = 0; - samples1 = 0; - - // Get 1 bit of data - for (sampleCount = 0; sampleCount < rfclk; sampleCount++){ - // Count number of even and odd high bits at center to edge - switch (pskcf) { - case 2: { - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 1])) { - if (pskcfidx == 0) { - samples0++; - } else { - samples1++; - } - } - break; - } - case 4: { - - // only check pskcf 2nd bit x 1 x x - if (pskcfidx == 1) { - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 2])) { - samples0++; - } else { - samples1++; - } - } - break; - } - case 8: { - - if (pskcfidx == 3) { // x x x 1 x x x x // 00041840 : FFFBE7BF - - // if current sample is high - if (DC(data[offset]) > DC(data[offset + 4])) { - samples0++; - } else { - samples1++; - } - } - break; - } - default: { - break; - } - } - - // If at bit boundary (after first bit) then adjust phase check for drift - if ((sampleCount > 0) && (sampleCount % rfclk) == 0) { - pskcfidx -= drift; - } - - offset++; - pskcfidx++; - pskcfidx = pskcfidx % pskcf; - } - - myblock <<= 1; - if (samples1 > samples0) { - myblock++; - } - } - - *block = myblock; -} - -static void t55xx_psk2_demod (int *data, uint8_t rfclk, uint8_t pskcf, uint32_t *block) { - // decode PSK - t55xx_psk1_demod (data, rfclk, pskcf, block); - - uint32_t new_block = 0; - uint8_t prev_phase = 1; - - // Convert to PSK2 - for (int8_t bit = 31; bit >= 0; bit--) { - - new_block <<= 1; - - if (((*block >> bit) & 1) != prev_phase) { - new_block++; - } - - prev_phase = ((*block >> bit) & 1); - } - - *block = new_block; -} - -static void t55xx_search_config_psk(int *d, int pskV) { - - for (uint8_t pskcf = 0; pskcf < 3; pskcf++) { - - for (uint8_t speedBits = 0; speedBits < 64; speedBits++) { - - uint8_t rfclk = rfclk = (2 * speedBits) + 2; - uint32_t block = 0; - - if (pskV == 1) { - t55xx_psk1_demod (d, rfclk, pskcf, &block); - } - - if (pskV == 2) { - t55xx_psk2_demod (d, rfclk, pskcf, &block); - } - - if (t55xx_is_valid_block0(block, rfclk, pskcf)) { - PrintAndLogEx(SUCCESS, "Valid config block [%08X] - rfclk [%d] - pskcf [%d]", block, rfclk, pskcf); - } - } - } -} -*/ t55xx_conf_block_t Get_t55xx_Config(void) { return config; @@ -420,6 +167,7 @@ static int CmdT55xxCloneHelp(const char *Cmd) { PrintAndLogEx(NORMAL, _GREEN_("lf presco clone")); PrintAndLogEx(NORMAL, _GREEN_("lf pyramid clone")); PrintAndLogEx(NORMAL, _GREEN_("lf securakey clone")); + PrintAndLogEx(NORMAL, _GREEN_("lf trovan clone")); PrintAndLogEx(NORMAL, _GREEN_("lf viking clone")); PrintAndLogEx(NORMAL, _GREEN_("lf visa2000 clone")); return PM3_SUCCESS; @@ -442,6 +190,7 @@ int clone_t55xx_tag(uint32_t *blockdata, uint8_t numblocks) { if (blockdata == NULL) return PM3_EINVARG; + if (numblocks < 1 || numblocks > 8) return PM3_EINVARG; @@ -477,12 +226,14 @@ int clone_t55xx_tag(uint32_t *blockdata, uint8_t numblocks) { if (i == 0) { SetConfigWithBlock0(blockdata[0]); - if (t55xxAcquireAndCompareBlock0(false, 0, blockdata[0], false)) + if (t55xxAcquireAndCompareBlock0(false, 0, blockdata[0], false)) { continue; + } } - if (t55xxVerifyWrite(i, 0, false, false, 0, 0xFF, blockdata[i]) == false) + if (t55xxVerifyWrite(i, 0, false, false, 0, 0xFF, blockdata[i]) == false) { res++; + } } if (res == 0) @@ -1301,6 +1052,24 @@ static int CmdT55xxDetect(const char *Cmd) { found = t55xxTryDetectModulation(downlink_mode, T55XX_PrintConfig); } + // With a tag on the antenna the psk2 / psk3 ambiguity can be settled rather + // than merely reported: read a few data blocks and apply the adjacent ones + // invariant. It needs the card, so it is skipped offline, where the single + // saved block 0 buffer cannot answer the question either way. + if (found && use_gb == false && t55xx_config_psk3_ambiguous()) { + + if (t55xx_psk3_probe(config.usepwd, config.pwd, config.downlink_mode)) { + config.modulation = DEMOD_PSK3; + PrintAndLogEx(SUCCESS, "Data blocks agree this is " _GREEN_("psk3") ", not psk2 - block 0 is one of the words listed above"); + } + + // put the configuration block back in the demod buffer, so anything + // reading it after us sees block 0 rather than the last probed block + if (AcquireData(T55x7_PAGE0, T55x7_CONFIGURATION_BLOCK, config.usepwd, config.pwd, config.downlink_mode)) { + DecodeT55xxBlock(); + } + } + if (found == false) { config.usepwd = false; config.pwd = 0x00; @@ -1315,6 +1084,494 @@ bool t55xxTryDetectModulation(uint8_t downlink_mode, bool print_config) { return t55xxTryDetectModulationEx(downlink_mode, print_config, 0, -1); } +#define PM3_T55_FALLBACK_MAXERR 100 + +static bool block0_repeats_at_stride(uint8_t offset) { + + if ((size_t)offset + 64 > g_DemodBufferLen || offset > 255 - 32) { + return false; + } + return (PackBits(offset, 32, g_DemodBuffer) == PackBits((uint8_t)(offset + 32), 32, g_DemodBuffer)); +} + +static void t55xx_psk_coherent(int fitclk, uint8_t clk, t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + static const int subcarriers[] = { 2, 4, 8 }; + + if (g_GraphTraceLen < 2048 || fitclk < 4) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + uint8_t *raw = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + uint8_t *work = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + if (raw == NULL || work == NULL) { + free(sig); + free(raw); + free(work); + return; + } + + const uint8_t before = *hits; + + size_t n = 0; + double got_clk = 0.0, best_score = -1.0; + int got_phase = 0; + + for (size_t s = 0; s < ARRAYLEN(subcarriers); s++) { + + size_t got_n = MAX_DEMOD_BUF_LEN; + double this_clk = 0.0, score = 0.0; + int this_phase = 0; + + if (pm3_psk_demod(sig, count, subcarriers[s], (double)fitclk, work, &got_n, &this_clk, &this_phase, &score, NULL) != PM3_SUCCESS) { + continue; + } + + if (score > best_score) { + best_score = score; + n = got_n; + got_clk = this_clk; + got_phase = this_phase; + memcpy(raw, work, got_n); + } + } + + if (n >= 32) { + + for (int variant = 0; variant < 4 && *hits == before; variant++) { + + const bool inverted = ((variant & 1) != 0); + + for (size_t i = 0; i < n; i++) { + work[i] = (inverted) ? (raw[i] ^ 1) : raw[i]; + } + + if (variant >= 2) { + psk1TOpsk2(work, n); + } + + setDemodBuff(work, n, 0); + setClockGrid((uint32_t)(got_clk + 0.5), got_phase); + + static const uint8_t modes[4] = { DEMOD_PSK1, DEMOD_PSK1, DEMOD_PSK2, DEMOD_PSK3 }; + + int bitRate = 0; + if (test(modes[variant], &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + + tests[*hits].modulation = modes[variant]; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = inverted; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + free(sig); + free(raw); + free(work); +} + +static void t55xx_ask_coherent(int fitclk, uint8_t clk, t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + if (g_GraphTraceLen < 2048 || fitclk < 4) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + uint8_t *raw = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + uint8_t *work = calloc(MAX_DEMOD_BUF_LEN, sizeof(uint8_t)); + if (raw == NULL || work == NULL) { + free(sig); + free(raw); + free(work); + return; + } + + const uint8_t before = *hits; + + // a transition code carries two chips per bit + size_t n = MAX_DEMOD_BUF_LEN; + double got_chip = 0.0; + int got_phase = 0; + + if (pm3_ask_chips(sig, count, (double)fitclk / 2.0, raw, &n, &got_chip, &got_phase) == PM3_SUCCESS) { + + // manchester first, then biphase at both pair alignments, each way up + for (int variant = 0; variant < 6 && *hits == before; variant++) { + + const int invert = (variant & 1); + size_t size = n; + uint8_t mode; + + memcpy(work, raw, n); + + if (variant < 2) { + uint8_t align = 0; + if (manrawdecode(work, &size, (uint8_t)invert, &align) == 0xFFFF) { + continue; + } + mode = DEMOD_ASK; + } else { + int offset = (variant < 4) ? 0 : 1; + if (BiphaseRawDecode(work, &size, &offset, invert) < 0) { + continue; + } + mode = invert ? DEMOD_BIa : DEMOD_BI; + } + + if (size < 32) { + continue; + } + + setDemodBuff(work, size, 0); + setClockGrid((uint32_t)((got_chip * 2.0) + 0.5), got_phase); + + int bitRate = 0; + if (test(mode, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + + tests[*hits].modulation = mode; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (invert != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + free(sig); + free(raw); + free(work); +} + +static bool t55xx_fallback_try(pm3_mod_t mod, pm3_enc_t enc, int fc_hi, int fc_lo, int fitclk, + t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode, + bool coherent_ok) { + + const uint8_t before = *hits; + int bitRate = 0; + + const uint8_t clk = (uint8_t)((fitclk > 0 && fitclk < 256) ? fitclk : 0); + + if (mod == PM3_MOD_FSK) { + + static const uint8_t rates[] = { 32, 40, 50, 64, 100, 128 }; + + const uint8_t pairs[3][2] = { + { (uint8_t)fc_hi, (uint8_t)fc_lo }, { 8, 5 }, { 10, 8 } + }; + + for (int strict = 1; strict >= 0; strict--) { + for (size_t p = 0; p < ARRAYLEN(pairs); p++) { + + if (pairs[p][0] == 0 || pairs[p][1] == 0) { + continue; + } + + for (size_t r = 0; r < ARRAYLEN(rates); r++) { + for (int inv = 0; inv < 2; inv++) { + + if (FSKrawDemod(rates[r], (uint8_t)inv, pairs[p][0], pairs[p][1], false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_FSK, &tests[*hits].offset, &bitRate, rates[r], &tests[*hits].Q5) == false) { + continue; + } + + if (strict && block0_repeats_at_stride(tests[*hits].offset) == false) { + continue; + } + + uint8_t m = DEMOD_FSK; + if (pairs[p][0] == 8 && pairs[p][1] == 5) { + m = inv ? DEMOD_FSK1 : DEMOD_FSK1a; + } else if (pairs[p][0] == 10 && pairs[p][1] == 8) { + m = inv ? DEMOD_FSK2a : DEMOD_FSK2; + } + + tests[*hits].modulation = m; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + } + } + } + return false; + } + + if (mod == PM3_MOD_PSK) { + + buffer_savestate_t saveState = save_bufferS32(g_GraphBuffer, g_GraphTraceLen); + saveState.offset = g_GridOffset; + CmdLtrim("-i 160"); + + for (int inv = 0; inv < 2; inv++) { + if (PSKDemod(fitclk, inv, PM3_T55_FALLBACK_MAXERR, false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_PSK1, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + tests[*hits].modulation = DEMOD_PSK1; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + break; + } + + // PSK2 and PSK3 are PSK1 put through psk1TOpsk2() + if (*hits == before && PSKDemod(fitclk, 0, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) { + psk1TOpsk2(g_DemodBuffer, g_DemodBufferLen); + if (test(DEMOD_PSK2, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + tests[*hits].modulation = DEMOD_PSK2; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + } + + restore_bufferS32(saveState, g_GraphBuffer); + g_GridOffset = saveState.offset; + + if (*hits == before && coherent_ok) { + t55xx_psk_coherent(fitclk, clk, tests, hits, downlink_mode); + } + return (*hits != before); + } + + if (mod == PM3_MOD_NRZ || (mod == PM3_MOD_ASK && enc == PM3_ENC_RAW)) { + + if (fitclk <= 8) { + return false; + } + + for (int inv = 0; inv < 2; inv++) { + if (NRZrawDemod(fitclk, inv, PM3_T55_FALLBACK_MAXERR, false) != PM3_SUCCESS) { + continue; + } + if (test(DEMOD_NRZ, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5) == false) { + continue; + } + tests[*hits].modulation = DEMOD_NRZ; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = (inv != 0); + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + return false; + } + + tests[*hits].ST = true; + if ((ASKDemod_ext(fitclk, 0, PM3_T55_FALLBACK_MAXERR, 0, false, false, false, 1, &tests[*hits].ST) == PM3_SUCCESS) + && test(DEMOD_ASK, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_ASK; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + tests[*hits].ST = true; + if ((ASKDemod_ext(fitclk, 1, PM3_T55_FALLBACK_MAXERR, 0, false, false, false, 1, &tests[*hits].ST) == PM3_SUCCESS) + && test(DEMOD_ASK, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_ASK; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = true; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + if ((ASKbiphaseDemod(0, fitclk, 0, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) + && test(DEMOD_BI, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_BI; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = false; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + return true; + } + + if ((ASKbiphaseDemod(0, fitclk, 1, PM3_T55_FALLBACK_MAXERR, false) == PM3_SUCCESS) + && test(DEMOD_BIa, &tests[*hits].offset, &bitRate, clk, &tests[*hits].Q5)) { + + tests[*hits].modulation = DEMOD_BIa; + tests[*hits].bitrate = bitRate; + tests[*hits].inverted = true; + tests[*hits].block0 = PackBits(tests[*hits].offset, 32, g_DemodBuffer); + tests[*hits].ST = false; + tests[*hits].downlink_mode = downlink_mode; + (*hits)++; + } + + if (*hits == before && coherent_ok) { + t55xx_ask_coherent(fitclk, clk, tests, hits, downlink_mode); + } + + return (*hits != before); +} + + +#define PM3_T55_FALLBACK_HYPS 4 + +static void t55xx_detect_fallback(t55xx_conf_block_t *tests, uint8_t *hits, uint8_t downlink_mode) { + + if (g_GraphTraceLen < 2048) { + return; + } + + size_t count = g_GraphTraceLen; + if (count > 16384) { + count = 16384; + } + + double *sig = pm3_extract(g_GraphBuffer, g_GraphTraceLen, 0, count); + if (sig == NULL) { + return; + } + + pm3_spec_analysis_t an; + pm3_fit_opts_t opts = {0}; + + if (pm3_analyse(sig, count, pm3_next_pow2(count), PM3_WIN_HANN, &an) == PM3_SUCCESS + && an.confidence >= PM3_CONF_MEDIUM) { + + switch (an.family) { + case PM3_FAM_FSK: + opts.mod_mask = 1 << PM3_MOD_FSK; + break; + case PM3_FAM_PSK: + opts.mod_mask = 1 << PM3_MOD_PSK; + break; + case PM3_FAM_ASK: + case PM3_FAM_MANCHESTER: + opts.mod_mask = (1 << PM3_MOD_ASK) | (1 << PM3_MOD_NRZ); + break; + case PM3_FAM_NRZ: + opts.mod_mask = (1 << PM3_MOD_NRZ) | (1 << PM3_MOD_ASK); + break; + case PM3_FAM_UNKNOWN: + break; + } + } + + const int first_mask = opts.mod_mask; + + for (int round = 0; round < 3; round++) { + + if (round == 1) { + if (first_mask == 0) { + continue; + } + opts.mod_mask = 0; + } + if (round == 2) { + opts.mod_mask = 0; + } + + pm3_fit_t fit; + if (pm3_fit_run(sig, count, &opts, &fit) != PM3_SUCCESS) { + continue; + } + + int seen_mod[PM3_T55_FALLBACK_HYPS], seen_enc[PM3_T55_FALLBACK_HYPS], seen_clk[PM3_T55_FALLBACK_HYPS]; + size_t nseen = 0; + bool done = false; + + for (size_t i = 0; i < fit.count && nseen < PM3_T55_FALLBACK_HYPS; i++) { + + const int fitclk = (int)(fit.items[i].clk_fine + 0.5); + if (fitclk <= 0 || fitclk > 255) { + continue; + } + + bool dup = false; + for (size_t j = 0; j < nseen; j++) { + if ((seen_mod[j] == (int)fit.items[i].mod) && (seen_enc[j] == (int)fit.items[i].enc) && (seen_clk[j] == fitclk)) { + dup = true; + break; + } + } + if (dup) { + continue; + } + + seen_mod[nseen] = (int)fit.items[i].mod; + seen_enc[nseen] = (int)fit.items[i].enc; + seen_clk[nseen] = fitclk; + nseen++; + + if (t55xx_fallback_try(fit.items[i].mod + , fit.items[i].enc + , fit.items[i].fc_hi + , fit.items[i].fc_lo + , fitclk + , tests + , hits + , downlink_mode + , (round == 2))) { + done = true; + break; + } + } + + pm3_fit_free(&fit); + + if (done) { + break; + } + } + + free(sig); +} + bool t55xxTryDetectModulationEx(uint8_t downlink_mode, bool print_config, uint32_t wanted_conf, uint64_t pwd) { t55xx_conf_block_t tests[15]; @@ -1482,6 +1739,11 @@ bool t55xxTryDetectModulationEx(uint8_t downlink_mode, bool print_config, uint32 // t55xx_search_config_psk(g_GraphBuffer, 2); } } + + if (hits == 0) { + t55xx_detect_fallback(tests, &hits, downlink_mode); + } + if (hits == 1) { config.modulation = tests[0].modulation; config.bitrate = tests[0].bitrate; @@ -1589,6 +1851,71 @@ bool GetT55xxBlockData(uint32_t *blockdata) { return true; } +static bool t55xx_has_adjacent_ones(uint32_t v) { + const uint32_t rot = (v >> 1) | ((v & 1) << 31); + return ((v & rot) != 0); +} + +static bool t55xx_config_psk3_ambiguous(void) { + + if (config.modulation != DEMOD_PSK2 || config.Q5) { + return false; + } + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + const uint8_t clk = basic[config.bitrate & 0x07]; + + uint32_t cand[T55XX_PSK3_MAX_CAND]; + return (t55xx_psk3_block0_candidates(config.block0, clk, cand, ARRAYLEN(cand)) > 0); +} + +#define T55XX_PSK3_PROBE_BLOCKS 7 +#define T55XX_PSK3_PROBE_TRIES 3 +#define T55XX_PSK3_PROBE_MIN 3 + +static bool t55xx_psk3_probe(bool usepwd, uint32_t password, uint8_t downlink_mode) { + + size_t usable = 0, clean = 0; + + for (uint8_t b = 1; b <= T55XX_PSK3_PROBE_BLOCKS; b++) { + + bool got = false, ok = false; + + for (uint8_t t = 0; t < T55XX_PSK3_PROBE_TRIES; t++) { + + if (AcquireData(T55x7_PAGE0, b, usepwd, password, downlink_mode) == false) { + continue; + } + if (DecodeT55xxBlock() == false) { + continue; + } + + uint32_t val = 0; + if (GetT55xxBlockData(&val) == false) { + continue; + } + + got = true; + + if (t55xx_has_adjacent_ones(val) == false) { + ok = true; + break; + } + } + + if (got == false) { + continue; + } + + usable++; + if (ok) { + clean++; + } + } + + return (usable >= T55XX_PSK3_PROBE_MIN && clean == usable); +} + void printT55xxBlock(uint8_t blockNum, bool page1) { uint32_t val = 0; @@ -1600,7 +1927,9 @@ void printT55xxBlock(uint8_t blockNum, bool page1) { T55x7_SaveBlockData((page1) ? blockNum + 8 : blockNum, val); - PrintAndLogEx(SUCCESS, " %02d | %08X | %s | %s", blockNum, val, sprint_bytebits_bin(g_DemodBuffer + config.offset, 32), sprint_ascii(bytes, 4)); + const char *note = t55xx_config_psk3_ambiguous() ? _YELLOW_(" <- psk2/psk3 ambiguous") : ""; + + PrintAndLogEx(SUCCESS, " %02d | %08X | %s | %s%s", blockNum, val, sprint_bytebits_bin(g_DemodBuffer + config.offset, 32), sprint_ascii(bytes, 4), note); } static bool testModulation(uint8_t mode, uint8_t modread) { @@ -1714,7 +2043,7 @@ static bool testQ5(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk) *fndBitRate = convertQ5bitRate(bitRate); if (*fndBitRate < 0) continue; - *offset = idx; + *offset = (uint8_t)idx; return true; } @@ -1729,31 +2058,69 @@ static bool testBitRate(uint8_t readRate, uint8_t clk) { return false; } -bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) { +typedef struct { + uint16_t last; // highest offset a whole window fits at + uint32_t val[256]; + uint16_t count[256]; // offsets in the buffer holding this same value + bool stride[256]; // and the same value again one block further on +} t55_windows_t; - if (g_DemodBufferLen < 64) { - return false; +static void windows_build(t55_windows_t *w) { + + memset(w, 0, sizeof(*w)); + + // offset is stored in a uint8_t, hence the 255 bound + w->last = (g_DemodBufferLen - 32 > 255) ? 255 : (uint16_t)(g_DemodBufferLen - 32); + + for (uint16_t i = 0; i <= w->last; i++) { + w->val[i] = PackBits((uint8_t)i, 32, g_DemodBuffer); } - for (uint8_t idx = 28; idx < 64; idx++) { + for (uint16_t i = 0; i <= w->last; i++) { - uint8_t si = idx; + for (uint16_t j = 0; j <= w->last; j++) { + if (w->val[j] == w->val[i]) { + w->count[i]++; + } + } + + if (i + 32 <= w->last) { + w->stride[i] = (w->val[i] == w->val[i + 32]); + } + } +} + +static bool test_scan(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, + uint16_t start, uint16_t end, const t55_windows_t *w, + uint16_t need, bool need_stride) { + + for (uint16_t idx = start; idx <= end; idx++) { + + uint8_t si = (uint8_t)idx; if (PackBits(si, 28, g_DemodBuffer) == 0x00) { continue; } + if (w->count[idx] < need || (need_stride && w->stride[idx] == false)) { + continue; + } + uint8_t safer = PackBits(si, 4, g_DemodBuffer); si += 4; //master key uint8_t resv = PackBits(si, 4, g_DemodBuffer); - si += 4; //was 7 & +=7+3 //should be only 4 bits if extended mode + si += 4; //was 7 & +=7+3 // should be only 4 bits if extended mode // 2nibble must be zeroed. - // moved test to here, since this gets most faults first. if (resv > 0x00) { continue; } + // The master key is 0, or 6 or 9 to select extended mode + if (safer != 0x0 && safer != 0x6 && safer != 0x9) { + continue; + } + int bitRate = PackBits(si, 6, g_DemodBuffer); si += 6; //bit rate (includes extended mode part of rate) uint8_t extend = PackBits(si, 1, g_DemodBuffer); @@ -1789,11 +2156,67 @@ bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) } *fndBitRate = bitRate; - *offset = idx; - *Q5 = false; + *offset = (uint8_t)idx; return true; } + return false; +} + +bool test(uint8_t mode, uint8_t *offset, int *fndBitRate, uint8_t clk, bool *Q5) { + + if (g_debugMode) { + PrintAndLogEx(DEBUG, "DEBUG (test) mode %u clk %u dclk %d len %zu : %s", mode, clk, g_DemodClock, g_DemodBufferLen, + sprint_bytebits_bin(g_DemodBuffer, (g_DemodBufferLen > 512) ? 512 : g_DemodBufferLen)); + } + + // One block is all it takes to carry a configuration. The old floor of 64 + // threw away every short demodulation unread, and a manchester rf/128 + // block read demodulates to 49 bits - the whole capture is only 93 bit + // periods long. + if (g_DemodBufferLen < 32) { + return false; + } + + // Scan as far as the buffer allows rather than stopping at bit 64. + // + // The tag repeats its configuration every 32 bits, so a window of 36 + // offsets covers barely one period - and if the first clean copy happens + // to start later than that, because the demodulator dropped a bit early on + // or the buffer opens mid block, the config is simply never looked at. + // A psk1 rf/32 capture had a perfectly good copy sitting past bit 64 while + // detection failed. offset is a uint8_t, hence the 255 bound. + const uint16_t limit = (g_DemodBufferLen - 32 > 255) ? 255 : (uint16_t)(g_DemodBufferLen - 32); + + // Where to start. + // + // Starting at 28 skips the ragged first copy, which is the right thing to + // do as long as what is left still spans a whole 32 bit period - that + // needs offsets up to 59, so 91 bits of buffer. Below that, starting at + // 28 means some phases are never looked at at all, and on the manchester + // rf/128 read the one that is never looked at is offset 0, where the + // configuration actually sits. + const uint16_t start = (g_DemodBufferLen >= 92) ? 28 : 0; + + t55_windows_t w; + windows_build(&w); + + // A word that had every chance to show itself twice and did not is a + // coincidence, and answering with it is worse than not answering. Only a + // buffer too short to have held a second copy gets to fall back on a + // single sighting. + const uint16_t need = (g_DemodBufferLen >= 64) ? 2 : 1; + + // Strongest corroboration first: the block stride, then a bare repeat. + const bool stride_pass[2] = { true, false }; + + for (uint8_t r = 0; r < 2; r++) { + if (test_scan(mode, offset, fndBitRate, clk, start, limit, &w, need, stride_pass[r])) { + *Q5 = false; + return true; + } + } + if (testQ5(mode, offset, fndBitRate, clk)) { *Q5 = true; return true; @@ -1833,6 +2256,121 @@ int CmdT55xxSpecial(const char *Cmd) { return PM3_SUCCESS; } +// Is `b` a word a T55x7 block 0 could actually hold, with modulation psk3 and +// the bit rate the demodulation settled on? +// +// Only rules that are certainly true are applied - master key, the fixed zero +// bits, the bit rate, modulation field 3 and a psk carrier that exists. A +// candidate list one entry too long is harmless; one that has dropped the real +// word is not, so nothing merely probable is tested here. +static bool t55xx_psk3_block0_plausible(uint32_t b, uint8_t clk) { + + const uint8_t master = (uint8_t)((b >> 28) & 0x0F); + const bool xmode = (((b >> 17) & 1) != 0) && (master == 0x6 || master == 0x9); + + if (xmode) { + if (b & 0x0F000000) { + return false; + } + } else { + if (master != 0x0) { + return false; + } + if (b & 0x0FE00106) { + return false; + } + } + + // modulation field 3 is psk3, which is the whole point of the enumeration + if (((b >> 12) & 0x1F) != 0x03) { + return false; + } + + // psk carrier 11 is reserved, so a word claiming it is not a real config + if (((b >> 10) & 0x03) == 0x03) { + return false; + } + + if (xmode) { + return (EM4x05_GET_BITRATE((b >> 18) & 0x3F) == clk); + } + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + return (basic[(b >> 18) & 0x07] == clk); +} + +static size_t t55xx_psk3_block0_candidates(uint32_t observed, uint8_t clk, uint32_t *out, size_t max) { + + if (out == NULL || max == 0 || observed == 0) { + return 0; + } + + uint8_t edge[32], nedge = 0; + for (uint8_t i = 0; i < 32; i++) { + if ((observed >> (31 - i)) & 1) { + edge[nedge++] = i; + } + } + + uint8_t gap[32]; + for (uint8_t k = 0; k < nedge; k++) { + const uint8_t nxt = edge[(k + 1) % nedge]; + gap[k] = (nedge == 1) ? 32 : (uint8_t)((32 + nxt - edge[k]) % 32); + if (gap[k] < 2) { + return 0; + } + } + + uint8_t len[32]; + for (uint8_t k = 0; k < nedge; k++) { + len[k] = 1; + } + + size_t found = 0; + + for (;;) { + + uint32_t d = 0; + for (uint8_t k = 0; k < nedge; k++) { + for (uint8_t t = 0; t < len[k]; t++) { + d |= 1u << (31 - ((edge[k] + t) % 32)); + } + } + + if (t55xx_psk3_block0_plausible(d, clk)) { + + bool dup = false; + for (size_t i = 0; i < found; i++) { + if (out[i] == d) { + dup = true; + break; + } + } + if (dup == false) { + out[found++] = d; + if (found == max) { + return found; + } + } + } + + uint8_t k = 0; + while (k < nedge) { + len[k]++; + if (len[k] < gap[k]) { + break; + } + len[k] = 1; + k++; + } + if (k == nedge) { + break; + } + } + + return found; +} + int printConfiguration(t55xx_conf_block_t b) { PrintAndLogEx(INFO, " Chip type......... " _GREEN_("%s"), (b.Q5) ? "Q5/T5555" : "T55x7"); PrintAndLogEx(INFO, " Modulation........ " _GREEN_("%s"), GetSelectedModulationStr(b.modulation)); @@ -1841,6 +2379,33 @@ int printConfiguration(t55xx_conf_block_t b) { PrintAndLogEx(INFO, " Offset............ %d", b.offset); PrintAndLogEx(INFO, " Seq. terminator... %s", (b.ST) ? _GREEN_("Yes") : "No"); PrintAndLogEx(INFO, " Block0............ %08X %s", b.block0, GetConfigBlock0Source(b.block0Status)); + + if (b.modulation == DEMOD_PSK2 && b.Q5 == false) { + + static const uint8_t basic[] = {8, 16, 32, 40, 50, 64, 100, 128}; + const uint8_t clk = basic[b.bitrate & 0x07]; + + uint32_t cand[T55XX_PSK3_MAX_CAND]; + size_t n = t55xx_psk3_block0_candidates(b.block0, clk, cand, ARRAYLEN(cand)); + + for (size_t i = 1; i < n; i++) { + uint32_t v = cand[i]; + size_t j = i; + while (j > 0 && cand[j - 1] > v) { + cand[j] = cand[j - 1]; + j--; + } + cand[j] = v; + } + + if (n > 0) { + PrintAndLogEx(INFO, " psk3 ambiguity.... this read is also consistent with a " _YELLOW_("psk3") " tag holding"); + for (size_t i = 0; i < n; i++) { + PrintAndLogEx(INFO, " %08X", cand[i]); + } + } + } + PrintAndLogEx(INFO, " Downlink mode..... %s", GetDownlinkModeStr(b.downlink_mode)); PrintAndLogEx(INFO, " Password set...... %s", (b.usepwd) ? _RED_("Yes") : _GREEN_("No")); if (b.usepwd) { @@ -4771,12 +5336,12 @@ static int CmdT55xxView(const char *Cmd) { } static command_t CommandTable[] = { - {"-----------", CmdHelp, AlwaysAvailable, "---------------------------- " _CYAN_("notice") " -----------------------------"}, - {"", CmdHelp, AlwaysAvailable, "Remember to run `" _YELLOW_("lf t55xx detect") "` first whenever a new card"}, - {"", CmdHelp, AlwaysAvailable, "is placed on the Proxmark3 or the config block changed."}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------- " _CYAN_("notice") " ---------------------------------"}, + {"", CmdHelp, AlwaysAvailable, "Always run `" _YELLOW_("lf t55xx detect") "` first whenever a new card is placed"}, + {"", CmdHelp, AlwaysAvailable, "on the Proxmark3 or the config block changed."}, {"", CmdHelp, AlwaysAvailable, ""}, {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"-----------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("operations") " ---------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "----------------------------- " _CYAN_("operations") " -------------------------------"}, {"clonehelp", CmdT55xxCloneHelp, IfPm3Lf, "Shows the available clone commands"}, {"config", CmdT55xxSetConfig, AlwaysAvailable, "Set/Get T55XX configuration (modulation, inverted, offset, rate)"}, {"dangerraw", CmdT55xxDangerousRaw, IfPm3Lf, "Sends raw bitstream. Dangerous, do not use!!"}, @@ -4786,13 +5351,13 @@ static command_t CommandTable[] = { {"info", CmdT55xxInfo, AlwaysAvailable, "Show T55x7 configuration data (page 0/ blk 0)"}, {"p1detect", CmdT55xxDetectPage1, IfPm3Lf, "Try detecting if this is a t55xx tag by reading page 1"}, {"read", CmdT55xxReadBlock, IfPm3Lf, "Read T55xx block data"}, - {"resetread", CmdResetRead, IfPm3Lf, "Send Reset Cmd then lf read the stream to attempt to identify the start of it"}, + {"resetread", CmdResetRead, IfPm3Lf, "Send Reset Cmd then lf read the stream to attempt to identify the start"}, {"restore", CmdT55xxRestore, IfPm3Lf, "Restore T55xx card Page 0 / Page 1 blocks"}, {"trace", CmdT55xxReadTrace, AlwaysAvailable, "Show T55x7 traceability data (page 1/ blk 0-1)"}, {"wakeup", CmdT55xxWakeUp, IfPm3Lf, "Send AOR wakeup command"}, {"view", CmdT55xxView, AlwaysAvailable, "Display content from tag dump file"}, {"write", CmdT55xxWriteBlock, IfPm3Lf, "Write T55xx block data"}, - {"-----------", CmdHelp, AlwaysAvailable, "--------------------- " _CYAN_("recovery") " ---------------------"}, + {"-----------", CmdHelp, AlwaysAvailable, "------------------------------ " _CYAN_("recovery") " --------------------------------"}, {"bruteforce", CmdT55xxBruteForce, IfPm3Lf, "Simple bruteforce attack to find password"}, {"chk", CmdT55xxChkPwds, IfPm3Lf, "Check passwords"}, {"protect", CmdT55xxProtect, IfPm3Lf, "Password protect tag"}, diff --git a/client/src/pm3line_vocabulary.h b/client/src/pm3line_vocabulary.h index 5237f7b57..d9063a2b1 100644 --- a/client/src/pm3line_vocabulary.h +++ b/client/src/pm3line_vocabulary.h @@ -95,6 +95,10 @@ const static vocabulary_t vocabulary[] = { { 1, "data manrawdecode" }, { 1, "data modulation" }, { 1, "data rawdemod" }, + { 1, "data autodemod" }, + { 1, "data fft" }, + { 1, "data fitscore" }, + { 1, "data spectrum" }, { 1, "data askedgedetect" }, { 1, "data autocorr" }, { 1, "data convertbitstream" }, @@ -124,6 +128,7 @@ const static vocabulary_t vocabulary[] = { { 0, "data hexsamples" }, { 0, "data samples" }, { 1, "data qrcode" }, + { 0, "data gensignal" }, { 0, "data test_ss8" }, { 0, "data test_ss32" }, { 0, "data test_ss32s" }, @@ -217,7 +222,6 @@ const static vocabulary_t vocabulary[] = { { 0, "hf 15 writeafi" }, { 0, "hf 15 writedsfid" }, { 0, "hf 15 csetuid" }, - { 0, "hf 15 cfinalize" }, { 1, "hf aliro help" }, { 1, "hf aliro list" }, { 0, "hf aliro info" }, @@ -923,6 +927,10 @@ const static vocabulary_t vocabulary[] = { { 1, "lf t55xx sniff" }, { 0, "lf t55xx special" }, { 0, "lf t55xx wipe" }, + { 1, "lf trovan help" }, + { 1, "lf trovan demod" }, + { 0, "lf trovan reader" }, + { 0, "lf trovan clone" }, { 1, "lf viking help" }, { 1, "lf viking demod" }, { 0, "lf viking reader" }, diff --git a/doc/commands.json b/doc/commands.json index 73cdca9ff..2a403e7ad 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -239,6 +239,28 @@ ], "usage": "data autocorr [-hg] [-w ]" }, + "data autodemod": { + "command": "data autodemod", + "description": "Tries to work out the modulation, encoding and clock of the wave in the GraphBuffer then run the matching demodulator Adds no signal processing of its own, the analysis is `data spectrum` and `data fitscore` A fractional clock is resampled onto an integer grid first", + "notes": [ + "data autodemod -> analyse and demodulate", + "data autodemod --dry-run -> decide and print, demodulate nothing", + "data autodemod --thres 6 -> insist on a 6 dB margin before trusting rank 1", + "data autodemod --invert --amp -> pass invert and amplify through to the demod" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size samples to analyse (def 16384)", + "--thres margin below which rank 1 is called ambiguous (def 3.0)", + "--dry-run analyse and decide, but do not demodulate", + "-a, --amp amplify the signal before ASK demodulation", + "-i, --invert invert the demodulated output", + "-v, --verbose show the demodulator's own output" + ], + "usage": "data autodemod [-haiv] [--start ] [--size ] [--thres ] [--dry-run]" + }, "data biphaserawdecode": { "command": "data biphaserawdecode", "description": "Biphase decode binary stream in DemodBuffer Converts 10 or 01 -> 1 and 11 or 00 -> 0 - must have binary sequence in DemodBuffer (run `data rawdemod --ar` before) - invert for Conditional Dephase Encoding (CDP) AKA Differential Manchester", @@ -407,6 +429,50 @@ ], "usage": "data envelope [-h]" }, + "data fft": { + "command": "data fft", + "description": "Fourier transform of the samples in the GraphBuffer. Reports the magnitude spectrum of the first N/2+1 bins, the input being real. The DC component is removed and the window is normalised to unit variance before the transform, so magnitudes are comparable between captures.", + "notes": [ + "data fft -> transform the whole buffer", + "data fft --size 4096 --win blackman -> 4096 point transform, blackman window", + "data fft --start 1000 --size 8192 --db -> magnitudes in dB relative to the peak", + "data fft --size 4096 --graph -> put the spectrum in the graph window" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size transform length, rounded up to a power of two (def largest that fits)", + "--win window function (def hann)", + "--db report magnitude in dB relative to the peak bin instead of linear", + "--graph write the magnitude spectrum into the GraphBuffer and repaint", + "--fs sample rate in Hz, adds a frequency column in Hz", + "--bins how many bins to print (def 32, 0 for all)" + ], + "usage": "data fft [-h] [--start ] [--size ] [--win ] [--db] [--graph] [--fs ] [--bins ]" + }, + "data fitscore": { + "command": "data fitscore", + "description": "Rank matched filter hypotheses against the GraphBuffer. A hypothesis is a modulation, a symbol shape and a clock. Each one is scored by correlating the trace against a single symbol template and measuring how tightly the correlator output clusters at the decision instants. Phase is not searched: the correlation is done with an FFT, which yields every phase offset at once. Templates are shaped by an assumed antenna Q, because correlating a bandlimited signal against square edges biases the ranking toward short clocks.", + "notes": [ + "data fitscore -> rank the whole bank", + "data fitscore --mod ask --top 5 -> ASK hypotheses only", + "data fitscore --clk 64 --all -> everything at clock 64", + "data fitscore --verbose -> also put the rank 1 correlator output in the graph" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size samples to analyse, rounded up to a power of two (def 16384)", + "--mod restrict to one modulation (def all)", + "--clk restrict to one clock (def all candidates)", + "--top rows to print (def 10)", + "--all print every scored hypothesis", + "-v, --verbose write the rank 1 correlator output into the GraphBuffer" + ], + "usage": "data fitscore [-hv] [--start ] [--size ] [--mod ] [--clk ] [--top ] [--all]" + }, "data fsktonrz": { "command": "data fsktonrz", "description": "Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) Omitted values are autodetect instead", @@ -423,6 +489,34 @@ ], "usage": "data fsktonrz [-h] [-c ] [--low ] [--hi ]" }, + "data gensignal": { + "command": "data gensignal", + "description": "Developer tool. Fill the GraphBuffer with a synthetic LF waveform. Used with `data fitscore` and `data autodemod`", + "notes": [ + "data gensignal --mod ask --enc manchester --clk 64", + "data gensignal --mod ask --enc manchester --clk 64.25 --noise 0.4 --len 20000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--mod modulation (def ask)", + "--enc encoding (def raw)", + "--clk clock in samples/symbol, may be fractional (def 64)", + "--len samples to generate (def 16384)", + "--noise gaussian noise standard deviation (def 0)", + "--drift linear DC ramp across the capture (def 0)", + "--env amplitude envelope swing (def 0)", + "--jitter per symbol edge jitter, samples (def 0)", + "--clip hard clip level, 0 for none (def 0)", + "--fc PSK subcarrier period (def 4)", + "--fchigh FSK long field clock (def 10)", + "--fclow FSK short field clock (def 8)", + "--q antenna Q to shape the waveform with (def 8)", + "--repeat loop a message of this many bits, like a real tag (def 0, random)", + "--seed RNG seed, for reproducible runs (def 1)" + ], + "usage": "data gensignal [-h] [--mod ] [--enc ] [--clk ] [--len ] [--noise ] [--drift ] [--env ] [--jitter ] [--clip ] [--fc ] [--fchigh ] [--fclow ] [--q ] [--repeat ] [--seed ]" + }, "data getbitstream": { "command": "data getbitstream", "description": "Convert GraphBuffer's value accordingly - larger or equal to ONE becomes ONE - less than ONE becomes ZERO", @@ -452,7 +546,7 @@ }, "data help": { "command": "data help", - "description": "help This help ----------- ------------------------- General------------------------- clear Clears various buffers used by the graph window hide Hide the graph window load Load contents of file into graph window num Converts dec/hex/bin plot Show the graph window print Print the data in the DemodBuffer save Save signal trace data setdebugmode Set Debugging Level on client side xor Xor a input string ----------- ------------------------- Modulation------------------------- biphaserawdecode Biphase decode bin stream in DemodBuffer detectclock Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer fsktonrz Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) manrawdecode Manchester decode binary stream in DemodBuffer modulation Identify LF signal for clock and modulation rawdemod Demodulate the data in the GraphBuffer and output binary ----------- ------------------------- Graph------------------------- askedgedetect Adjust Graph for manual ASK demod autocorr Autocorrelation over window convertbitstream Convert GraphBuffer's 0/1 values to 127 / -127 cthreshold Average out all values between dirthreshold Max rising higher up-thres/ Min falling lower down-thres decimate Decimate samples envelope Generate square envelope of samples grid overlay grid on graph window getbitstream Convert GraphBuffer's >=1 values to 1 and <1 to 0 hpf Remove DC offset from trace iir Apply IIR buttersworth filter on plot data ltrim Trim samples from left of trace mtrim Trim out samples from the specified start to the specified stop norm Normalize max/min to +/-128 rtrim Trim samples from right of trace setgraphmarkers Set the markers in the graph window shiftgraphzero Shift 0 for Graphed wave + or - shift value timescale Set cursor display timescale undecimate Un-decimate samples zerocrossings Count time between zero-crossings ----------- ------------------------- Operations------------------------- asn1 ASN1 decoder atr ATR lookup bmap Convert hex value according a binary template crypto Encrypt and decrypt data diff Diff of input files qrcode Create a QR code --------------------------------------------------------------------------------------- data clear available offline: yes This function clears the BigBuf on device side and graph window ( graphbuffer )", + "description": "help This help ----------- ------------------------------ General ------------------------------- clear Clears various buffers used by the graph window hide Hide the graph window load Load contents of file into graph window num Converts dec/hex/bin plot Show the graph window print Print the data in the DemodBuffer save Save signal trace data setdebugmode Set Debugging Level on client side xor Xor a input string ----------- ----------------------------- Modulation ----------------------------- biphaserawdecode Biphase decode bin stream in DemodBuffer detectclock Detect ASK, FSK, NRZ, PSK clock rate of wave in GraphBuffer fsktonrz Convert fsk2 to nrz wave for alternate fsk demodulating (for weak fsk) manrawdecode Manchester decode binary stream in DemodBuffer modulation Identify LF signal for clock and modulation rawdemod Demodulate the data in the GraphBuffer and output binary ----------- -------------------------- Frequency domain -------------------------- autodemod Detect modulation, encoding and clock, then demodulate fft Fourier transform of the GraphBuffer fitscore Rank matched filter hypotheses for modulation, encoding and clock spectrum Spectral peaks, symbol rate and modulation family hint ----------- ------------------------------- Graph -------------------------------- askedgedetect Adjust Graph for manual ASK demod autocorr Autocorrelation over window convertbitstream Convert GraphBuffer's 0/1 values to 127 / -127 cthreshold Average out all values between dirthreshold Max rising higher up-thres/ Min falling lower down-thres decimate Decimate samples envelope Generate square envelope of samples grid overlay grid on graph window getbitstream Convert GraphBuffer's >=1 values to 1 and <1 to 0 hpf Remove DC offset from trace iir Apply IIR buttersworth filter on plot data ltrim Trim samples from left of trace mtrim Trim out samples from the specified start to the specified stop norm Normalize max/min to +/-128 rtrim Trim samples from right of trace setgraphmarkers Set the markers in the graph window shiftgraphzero Shift 0 for Graphed wave + or - shift value timescale Set cursor display timescale undecimate Un-decimate samples zerocrossings Count time between zero-crossings ----------- ---------------------------- Operations ------------------------------ asn1 ASN1 decoder atr ATR lookup bmap Convert hex value according a binary template crypto Encrypt and decrypt data diff Diff of input files qrcode Create a QR code --------------------------------------------------------------------------------------- data clear available offline: yes This function clears the BigBuf on device side and graph window ( graphbuffer )", "notes": [ "data clear" ], @@ -774,6 +868,29 @@ ], "usage": "data shiftgraphzero [-h] -n " }, + "data spectrum": { + "command": "data spectrum", + "description": "Spectral analysis of the GraphBuffer. Reports the strongest spectral peaks with a sub bin estimate of the symbol clock, the squaring and delay-and-multiply spectra which expose rates the plain spectrum nulls out, and a modulation family hint derived from spectral shape alone. Run this on a trace that will not decode.", + "notes": [ + "data spectrum -> peak table and family hint", + "data spectrum --top 8 --sq -> more peaks, plus the squaring spectrum", + "data spectrum --stft --size 2048 -> spectrogram over the whole capture", + "data spectrum --stft --size 2048 --hop 256 -> finer time resolution" + ], + "offline": true, + "options": [ + "-h, --help This help", + "--start first sample of the GraphBuffer to use (def 0)", + "--size transform length, rounded up to a power of two (def largest that fits)", + "--win window function (def hann)", + "--top number of peaks to report (def 5)", + "--sq also show the squaring and delay-and-multiply spectra", + "--stft sliding window spectrogram, ridge tracking and drift", + "--hop STFT hop in samples (def size/4)", + "--fs sample rate in Hz, adds a frequency column in Hz" + ], + "usage": "data spectrum [-h] [--start ] [--size ] [--win ] [--top ] [--sq] [--stft] [--hop ] [--fs ]" + }, "data test_ss32": { "command": "data test_ss32", "description": "Tests the implementation of Buffer Save States (32-bit buffer)", @@ -1774,35 +1891,20 @@ ], "usage": "hf 14b wrbl [-h] [-b ] -d [--512] [--4k] [--sb] [--force]" }, - "hf 15 cfinalize": { - "command": "hf 15 cfinalize", - "description": "Finalize a magic ISO15693 'V3' tag. This operation is irreversible. After finalize the configuration area is erased and the UID can no longer be changed. Set the UID with `hf 15 csetuid --v3` first, then lock it in with this command.", - "notes": [ - "hf 15 cfinalize -y" - ], - "offline": false, - "options": [ - "-h, --help This help", - "-y, --yes Confirm the irreversible finalize operation" - ], - "usage": "hf 15 cfinalize [-hy]" - }, "hf 15 csetuid": { "command": "hf 15 csetuid", - "description": "Set UID for magic Chinese card (only works with such cards) For magic 'V3' tags this writes the UID configuration only and is repeatable; run `hf 15 cfinalize` afterwards to lock the UID permanently.", + "description": "Set UID for magic Chinese card (only works with such cards)", "notes": [ "hf 15 csetuid -u E011223344556677 -> use gen1 command", - "hf 15 csetuid -u E011223344556677 --v2 -> use gen2 command", - "hf 15 csetuid -u E011223344556677 --v3 -> use gen3 (V3) magic tag" + "hf 15 csetuid -u E011223344556677 --v2 -> use gen2 command" ], "offline": false, "options": [ "-h, --help This help", "-u, --uid UID, 8 hex bytes", - "-2, --v2 Use gen2 magic command", - "-3, --v3 Use gen3 (V3) magic tag (repeatable, needs cfinalize)" + "-2, --v2 Use gen2 magic command" ], - "usage": "hf 15 csetuid [-h23] -u " + "usage": "hf 15 csetuid [-h2] -u " }, "hf 15 demod": { "command": "hf 15 demod", @@ -11997,7 +12099,7 @@ }, "lf help": { "command": "lf help", - "description": "help This help ----------- -------------- Low Frequency -------------- awid { AWID RFIDs... } cotag { COTAG CHIPs... } destron { FDX-A Destron RFIDs... } em { EM CHIPs & RFIDs... } fdxb { FDX-B RFIDs... } gallagher { GALLAGHER RFIDs... } gproxii { Guardall Prox II RFIDs... } hid { HID Prox RFIDs... } hitag { Hitag CHIPs... } idteck { Idteck RFIDs... } indala { Indala RFIDs... } io { ioProx RFIDs... } jablotron { Jablotron RFIDs... } keri { KERI RFIDs... } motorola { Motorola Flexpass RFIDs... } nedap { Nedap RFIDs... } nexwatch { NexWatch RFIDs... } noralsy { Noralsy RFIDs... } pac { PAC/Stanley RFIDs... } paradox { Paradox RFIDs... } pcf7931 { PCF7931 CHIPs... } presco { Presco RFIDs... } pyramid { Farpointe/Pyramid RFIDs... } securakey { Securakey RFIDs... } ti { TI CHIPs... } t55xx { T55xx CHIPs... } viking { Viking RFIDs... } visa2000 { Visa2000 RFIDs... } ----------- --------------------- General --------------------- search Read and Search for valid known tag --------------------------------------------------------------------------------------- lf config available offline: no Get/Set config for LF sampling, bit/sample, decimation, frequency These changes are temporary, will be reset after a power cycle. - use `lf read` performs a read (active field) - use `lf sniff` performs a sniff (no active field)", + "description": "help This help ----------- -------------- Low Frequency -------------- awid { AWID RFIDs... } cotag { COTAG CHIPs... } destron { FDX-A Destron RFIDs... } em { EM CHIPs & RFIDs... } fdxb { FDX-B RFIDs... } gallagher { GALLAGHER RFIDs... } gproxii { Guardall Prox II RFIDs... } hid { HID Prox RFIDs... } hitag { Hitag CHIPs... } idteck { Idteck RFIDs... } indala { Indala RFIDs... } io { ioProx RFIDs... } jablotron { Jablotron RFIDs... } keri { KERI RFIDs... } motorola { Motorola Flexpass RFIDs... } nedap { Nedap RFIDs... } nexwatch { NexWatch RFIDs... } noralsy { Noralsy RFIDs... } pac { PAC/Stanley RFIDs... } paradox { Paradox RFIDs... } pcf7931 { PCF7931 CHIPs... } presco { Presco RFIDs... } pyramid { Farpointe/Pyramid RFIDs... } securakey { Securakey RFIDs... } ti { TI CHIPs... } t55xx { T55xx CHIPs... } trovan { Trovan animal IDs... } viking { Viking RFIDs... } visa2000 { Visa2000 RFIDs... } ----------- --------------------- General --------------------- search Read and Search for valid known tag --------------------------------------------------------------------------------------- lf config available offline: no Get/Set config for LF sampling, bit/sample, decimation, frequency These changes are temporary, will be reset after a power cycle. - use `lf read` performs a read (active field) - use `lf sniff` performs a sniff (no active field)", "notes": [ "lf config -> shows current config", "lf config -b 8 --125 -> samples at 125 kHz, 8 bps", @@ -14222,6 +14324,50 @@ ], "usage": "lf ti write [-h] -r [--crc ]" }, + "lf trovan clone": { + "command": "lf trovan clone", + "description": "clone a Trovan animal ID to a T55x7, Q5/T5555 or EM4305/4469 tag The ID is the 10 hex digits printed on the tag, with or without dashes.", + "notes": [ + "lf trovan clone --id 0007F9043A", + "lf trovan clone --id 00-07F9-043A", + "lf trovan clone --id 0007F9043A --q5" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--id Trovan animal ID, 10 hex digits", + "--q5 specify writing to Q5/T5555 tag", + "--em specify writing to EM4305/4469 tag" + ], + "usage": "lf trovan clone [-h] --id [--q5] [--em]" + }, + "lf trovan help": { + "command": "lf trovan help", + "description": "help This help demod demodulate a Trovan tag from the GraphBuffer --------------------------------------------------------------------------------------- lf trovan demod available offline: yes Demodulate a Trovan animal ID tag from the GraphBuffer", + "notes": [ + "lf trovan demod" + ], + "offline": true, + "options": [ + "-h, --help This help", + "-v, --verbose verbose output" + ], + "usage": "lf trovan demod [-hv]" + }, + "lf trovan reader": { + "command": "lf trovan reader", + "description": "read a Trovan animal ID tag", + "notes": [ + "lf trovan reader -@ -> continuous reader mode" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-@ optional - continuous reader mode", + "-v, --verbose verbose output" + ], + "usage": "lf trovan reader [-h@v]" + }, "lf tune": { "command": "lf tune", "description": "Continuously measure LF antenna tuning. Press button or to interrupt.", @@ -15873,8 +16019,8 @@ } }, "metadata": { - "commands_extracted": 889, + "commands_extracted": 896, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-07-26T21:37:31+00:00" + "extracted_on": "2026-07-28T11:00:12+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index c63ddb6c7..d56072456 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -114,6 +114,10 @@ Check column "offline" for their availability. |`data manrawdecode `|Y |`Manchester decode binary stream in DemodBuffer` |`data modulation `|Y |`Identify LF signal for clock and modulation` |`data rawdemod `|Y |`Demodulate the data in the GraphBuffer and output binary` +|`data autodemod `|Y |`Detect modulation, encoding and clock, then demodulate` +|`data fft `|Y |`Fourier transform of the GraphBuffer` +|`data fitscore `|Y |`Rank matched filter hypotheses for modulation, encoding and clock` +|`data spectrum `|Y |`Spectral peaks, symbol rate and modulation family hint` |`data askedgedetect `|Y |`Adjust Graph for manual ASK demod` |`data autocorr `|Y |`Autocorrelation over window` |`data convertbitstream `|Y |`Convert GraphBuffer's 0/1 values to 127 / -127` @@ -143,6 +147,7 @@ Check column "offline" for their availability. |`data hexsamples `|N |`Dump big buffer as hex bytes` |`data samples `|N |`Get raw samples for graph window ( GraphBuffer )` |`data qrcode `|Y |`Create a QR code` +|`data gensignal `|N |`Generate a synthetic LF waveform into the GraphBuffer` |`data test_ss8 `|N |`Test the implementation of Buffer Save States (8-bit buffer)` |`data test_ss32 `|N |`Test the implementation of Buffer Save States (32-bit buffer)` |`data test_ss32s `|N |`Test the implementation of Buffer Save States (32-bit signed buffer)` @@ -276,7 +281,6 @@ Check column "offline" for their availability. |`hf 15 writeafi `|N |`Writes the AFI on an ISO-15693 tag` |`hf 15 writedsfid `|N |`Writes the DSFID on an ISO-15693 tag` |`hf 15 csetuid `|N |`Set UID for magic card` -|`hf 15 cfinalize `|N |`Finalize a magic V3 tag (irreversible)` ### hf aliro @@ -1521,6 +1525,18 @@ Check column "offline" for their availability. |`lf t55xx wipe `|N |`Wipe a T55xx tag and set defaults (will destroy any data on tag)` +### lf trovan + + { Trovan animal IDs... } + +|command |offline |description +|------- |------- |----------- +|`lf trovan help `|Y |`This help` +|`lf trovan demod `|Y |`demodulate a Trovan tag from the GraphBuffer` +|`lf trovan reader `|N |`attempt to read and extract tag data` +|`lf trovan clone `|N |`clone Trovan tag to T55x7 or Q5/T5555` + + ### lf viking { Viking RFIDs... }