From d1c37b83e0e097711d2190ee48d57a5c3268d356 Mon Sep 17 00:00:00 2001 From: Indiana Holmes Date: Fri, 11 Sep 2026 00:17:57 +0000 Subject: [PATCH] Add theme engine, animated UI, and Bus Dump / Crypto / Exfil modules UI: - Theme engine (5 palettes) persisted to NVS; live Theme switcher module - Animated shell: boot splash, CRT scanlines, breathing menu cursor, wipe-in transitions, spinners, progress bars New recon modules: - Bus Dump: SPI-NOR + I2C EEPROM readout to microSD (read-only) - Crypto Lab: entropy, magic-byte file-ID, XOR brute, AES-128 known-key - Exfil: SD browser + base64 dump export over USB serial, manifest writer README updated with the new module table and look-and-feel notes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AAhMHMRAQLQ9hSbBECKNfn --- README.md | 10 +++ src/core/shell.cpp | 41 +++++++++--- src/core/theme.cpp | 33 ++++++++++ src/core/theme.h | 20 ++++++ src/core/ui.cpp | 116 +++++++++++++++++++++++++++------- src/core/ui.h | 35 ++++++++--- src/modules/busdump.cpp | 126 +++++++++++++++++++++++++++++++++++++ src/modules/crypto.cpp | 129 ++++++++++++++++++++++++++++++++++++++ src/modules/exfil.cpp | 104 ++++++++++++++++++++++++++++++ src/modules/themepick.cpp | 43 +++++++++++++ 10 files changed, 615 insertions(+), 42 deletions(-) create mode 100644 src/core/theme.cpp create mode 100644 src/core/theme.h create mode 100644 src/modules/busdump.cpp create mode 100644 src/modules/crypto.cpp create mode 100644 src/modules/exfil.cpp create mode 100644 src/modules/themepick.cpp diff --git a/README.md b/README.md index 6fb3424..9721a53 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ OBD/USB port, a PC, smart appliances. | **UART Sniff** | Passive UART capture (hex+ascii, selectable baud); freeze a frame and **replay it on a keypress** (manual TX only). | | **USB Enum**| ESP32-S3 as USB host — reads **descriptors** (VID/PID, class, strings, config) to fingerprint an unknown device. Read-only. | | **DefCred** | Checks a single host (default: gateway) against a short list of **factory-default logins** over HTTP Basic Auth. Stops on first hit. | +| **Bus Dump**| Reads **SPI-NOR (25-series)** and **I2C EEPROM (24-series)** chips on your own boards to `/dump/*.bin` on microSD. Read-only. | +| **Crypto Lab** | Offline analysis of a dumped blob: **Shannon entropy**, magic-byte **file-ID**, single-byte **XOR** brute, **AES-128-ECB** known-key decrypt. | +| **Exfil** | Browse the SD card and **stream a dump to the host** over USB serial as base64 (`base64 -d`); writes a manifest of what you pulled. | +| **Theme** | Live **UI theme switcher** — 5 palettes, saved to NVS, restyles everything instantly. | + +## Look & feel + +Boot splash with a sweeping-glow logo, CRT scanlines, a breathing menu +cursor, wipe-in transitions, spinners and progress bars. Switch palettes any +time in the **Theme** module. ## Build & flash diff --git a/src/core/shell.cpp b/src/core/shell.cpp index 2a7f2bc..198bfee 100644 --- a/src/core/shell.cpp +++ b/src/core/shell.cpp @@ -1,39 +1,56 @@ #include "shell.h" #include "ui.h" +#include "theme.h" #include -// Module factories (defined in each module .cpp). Module* makePinScan(); Module* makeVSense(); Module* makeUartSniff(); Module* makeUsbEnum(); Module* makeDefCred(); +Module* makeBusDump(); +Module* makeCrypto(); +Module* makeExfil(); +Module* makeThemePicker(); void Shell::begin() { + theme::load(); add(makePinScan()); add(makeVSense()); add(makeUartSniff()); add(makeUsbEnum()); add(makeDefCred()); - M5.Display.fillScreen(ui::bg()); + add(makeBusDump()); + add(makeCrypto()); + add(makeExfil()); + add(makeThemePicker()); + ui::bootSplash(); drawMenu(); } void Shell::drawMenu() { - M5.Display.fillScreen(ui::bg()); + auto& d = M5.Display; + d.fillScreen(ui::bg()); ui::titleBar("Card-Crack", CARDCRACK_VERSION); + // animated selection glow bar + int y = ui::BODY_Y + sel * 12; + uint16_t sb = ui::mix(theme::active().panel, theme::active().accent, 0.25f + 0.25f * ui::pulse(900)); + d.fillRect(0, y, ui::W, 12, sb); for (int i = 0; i < nmods; i++) { bool cur = i == sel; - ui::lineC(i, cur ? ui::accent() : ui::fg(), "%c %-10s %s", - cur ? '>' : ' ', mods[i]->name(), mods[i]->blurb()); + uint16_t c = cur ? ui::accent() : ui::fg(); + d.setTextColor(c, cur ? sb : ui::bg()); + d.setCursor(5, ui::BODY_Y + i * 12 + 1); + d.printf("%c %-9s %s", cur ? char(0x10) : ' ', mods[i]->name(), mods[i]->blurb()); } - ui::hintBar("arrows move [enter]open home-lab only"); + ui::hintBar("; up . down enter open home-lab only"); } void Shell::enter(int i) { active = i; M5.Display.fillScreen(ui::bg()); ui::titleBar(mods[i]->name(), mods[i]->blurb()); + ui::wipeIn(); mods[i]->onEnter(); mods[i]->draw(); } @@ -52,8 +69,8 @@ void Shell::loop() { for (char c : st.word) { if (c == '`') { if (active >= 0) back(); continue; } if (active < 0) { - if (c == ';') { sel = (sel - 1 + nmods) % nmods; drawMenu(); } // up - else if (c == '.') { sel = (sel + 1) % nmods; drawMenu(); } // down + if (c == ';') { sel = (sel - 1 + nmods) % nmods; drawMenu(); } + else if (c == '.') { sel = (sel + 1) % nmods; drawMenu(); } else if (c == '\r' || c == ' ') enter(sel); } else { mods[active]->onKey(c); @@ -62,8 +79,12 @@ void Shell::loop() { if (st.enter && active < 0) enter(sel); } - // ~30 Hz tick for the active module. - if (active >= 0 && millis() - lastTick > 33) { + if (active < 0) { + // keep the menu selection breathing + if (millis() - lastTick > 60) { lastTick = millis(); drawMenu(); } + return; + } + if (millis() - lastTick > 33) { lastTick = millis(); mods[active]->tick(); mods[active]->draw(); diff --git a/src/core/theme.cpp b/src/core/theme.cpp new file mode 100644 index 0000000..f10cc4f --- /dev/null +++ b/src/core/theme.cpp @@ -0,0 +1,33 @@ +#include "theme.h" +#include + +namespace theme { +// 0xRRGGBB packed; ui converts to 565. +const Theme LIST[] = { + {"Matrix", 0x0A0E0A, 0x10201410 & 0xFFFFFF, 0xB8F0B8, 0x2E6B2E, 0x39FF6A, 0xE6C23C, 0xE05050, 0x1FBF4F}, + {"Vapor", 0x140A1E, 0x241436, 0xF0D8FF, 0x6E4E8E, 0xFF5FD0, 0xFFD25F, 0xFF5F87, 0x9F5FFF}, + {"Amber", 0x140F04, 0x2A2008, 0xFFD79A, 0x8A6A2E, 0xFFB020, 0xFFE070, 0xFF6030, 0xFF8A1F}, + {"Ice", 0x061018, 0x0E2030, 0xCDE8FF, 0x3E6E90, 0x40C8FF, 0xC8E060, 0xFF6A8A, 0x1F9FE0}, + {"Blood", 0x140606, 0x2A0E0E, 0xFFC8C8, 0x8A3030, 0xFF3B3B, 0xFFB020, 0xFF5F5F, 0xE01F1F}, +}; +const int COUNT = sizeof(LIST) / sizeof(LIST[0]); + +static int cur = 0; +static Preferences prefs; + +int current() { return cur; } +const Theme& active() { return LIST[cur]; } + +void set(int i) { + cur = (i % COUNT + COUNT) % COUNT; + prefs.begin("cardcrack", false); + prefs.putInt("theme", cur); + prefs.end(); +} +void load() { + prefs.begin("cardcrack", true); + cur = prefs.getInt("theme", 0); + prefs.end(); + if (cur < 0 || cur >= COUNT) cur = 0; +} +} diff --git a/src/core/theme.h b/src/core/theme.h new file mode 100644 index 0000000..e477e49 --- /dev/null +++ b/src/core/theme.h @@ -0,0 +1,20 @@ +#pragma once +#include + +// A theme is a named palette. Modules never hardcode colors — they pull +// from the active theme via ui::th(). Switch themes in the Theme module; +// the choice persists to NVS so it survives a reboot. + +struct Theme { + const char* name; + uint32_t bg, panel, fg, dim, accent, warn, bad, glow; +}; + +namespace theme { +extern const Theme LIST[]; +extern const int COUNT; +int current(); +void set(int i); // clamps + saves to NVS +const Theme& active(); +void load(); // restore from NVS at boot +} diff --git a/src/core/ui.cpp b/src/core/ui.cpp index 227cc60..6205e92 100644 --- a/src/core/ui.cpp +++ b/src/core/ui.cpp @@ -1,53 +1,125 @@ #include "ui.h" #include +#include namespace ui { +uint16_t rgb(uint32_t c) { + return M5.Display.color565((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF); +} + +uint16_t mix(uint32_t a, uint32_t b, float t) { + if (t < 0) t = 0; if (t > 1) t = 1; + int ar=(a>>16)&0xFF, ag=(a>>8)&0xFF, ab=a&0xFF; + int br=(b>>16)&0xFF, bg2=(b>>8)&0xFF, bb=b&0xFF; + return M5.Display.color565(ar+(br-ar)*t, ag+(bg2-ag)*t, ab+(bb-ab)*t); +} + +float pulse(uint32_t periodMs) { + float ph = (millis() % periodMs) / (float)periodMs; + return 0.5f - 0.5f * cosf(ph * 2.0f * PI); // smooth 0..1..0 +} + +static uint16_t titleBg() { return rgb(theme::active().panel); } + void titleBar(const char* title, const char* blurb) { auto& d = M5.Display; - d.fillRect(0, 0, W, BODY_Y, d.color565(24, 28, 34)); - d.setTextColor(accent(), d.color565(24, 28, 34)); + d.fillRect(0, 0, W, BODY_Y, titleBg()); + // pulsing accent tick on the left + d.fillRect(0, 0, 3, BODY_Y, mix(theme::active().accent, theme::active().glow, pulse(1400))); d.setTextSize(1); - d.setCursor(3, 4); - d.print(title); + d.setTextColor(accent(), titleBg()); + d.setCursor(7, 4); d.print(title); if (blurb && *blurb) { - d.setTextColor(fg(), d.color565(24, 28, 34)); - // Right-align-ish: just continue after a separator. - d.print(" "); - d.print(blurb); + d.setTextColor(dim(), titleBg()); + d.print(" "); d.print(blurb); } } void hintBar(const char* hint) { auto& d = M5.Display; int y = H - 12; - d.fillRect(0, y, W, 12, d.color565(24, 28, 34)); - d.setTextColor(d.color565(150, 156, 164), d.color565(24, 28, 34)); - d.setTextSize(1); - d.setCursor(3, y + 2); - d.print(hint); + d.fillRect(0, y, W, 12, titleBg()); + d.setTextColor(dim(), titleBg()); + d.setTextSize(1); d.setCursor(3, y + 2); d.print(hint); } -void clearBody() { - M5.Display.fillRect(0, BODY_Y, W, BODY_H, bg()); -} +void clearBody() { M5.Display.fillRect(0, BODY_Y, W, BODY_H, bg()); } static void vline(int row, uint16_t color, const char* fmt, va_list ap) { - char buf[64]; - vsnprintf(buf, sizeof(buf), fmt, ap); + char buf[64]; vsnprintf(buf, sizeof(buf), fmt, ap); auto& d = M5.Display; int y = BODY_Y + row * 12; d.fillRect(0, y, W, 12, bg()); d.setTextColor(color, bg()); - d.setTextSize(1); - d.setCursor(3, y + 1); - d.print(buf); + d.setTextSize(1); d.setCursor(5, y + 1); d.print(buf); } - void line(int row, const char* fmt, ...) { va_list ap; va_start(ap, fmt); vline(row, fg(), fmt, ap); va_end(ap); } void lineC(int row, uint16_t color, const char* fmt, ...) { va_list ap; va_start(ap, fmt); vline(row, color, fmt, ap); va_end(ap); } + +void bar(int row, float frac, uint16_t col, const char* label) { + if (frac < 0) frac = 0; if (frac > 1) frac = 1; + auto& d = M5.Display; + int y = BODY_Y + row * 12, x = 5, w = W - 60, h = 8; + d.fillRect(0, y, W, 12, bg()); + d.drawRect(x, y + 1, w, h, dim()); + d.fillRect(x + 1, y + 2, (int)((w - 2) * frac), h - 2, col); + d.setTextColor(fg(), bg()); d.setCursor(x + w + 4, y + 1); + d.printf("%3d%% %s", (int)(frac * 100), label ? label : ""); +} + +void spinner(int x, int y, uint16_t col) { + static const char* fr = "|/-\\"; + M5.Display.setTextColor(col, bg()); + M5.Display.setCursor(x, y); + M5.Display.print(fr[(millis() / 90) % 4]); +} + +// CRT-ish scanlines over the body — cheap, drawn every other row. +void scanlines() { + auto& d = M5.Display; + for (int y = BODY_Y; y < BODY_Y + BODY_H; y += 3) + d.drawFastHLine(0, y, W, mix(theme::active().bg, 0x000000, 0.35f)); +} + +void bootSplash() { + auto& d = M5.Display; + const char* logo = "CARD-CRACK"; + d.fillScreen(bg()); + // sweep the glow across the logo + for (int t = 0; t <= 24; t++) { + d.setTextSize(2); + int lw = strlen(logo) * 12, x = (W - lw) / 2, y = 44; + for (int i = 0; logo[i]; i++) { + float d2 = fabsf((i * 12) - (t * (lw / 24.0f))); + uint16_t c = mix(theme::active().dim, theme::active().glow, 1.0f - d2 / 40.0f); + d.setTextColor(c, bg()); d.setCursor(x + i * 12, y); d.print(logo[i]); + } + d.setTextSize(1); + d.setTextColor(dim(), bg()); + d.setCursor((W - 20 * 6) / 2 + 20, 74); + d.print("home-lab recon toolkit"); + // loading ticks + int bw = 140, bx = (W - bw) / 2; + d.drawRect(bx, 92, bw, 6, dim()); + d.fillRect(bx + 1, 93, (bw - 2) * t / 24, 4, accent()); + delay(28); + } + delay(180); + scanlines(); + delay(120); +} + +void wipeIn() { + auto& d = M5.Display; + for (int x = 0; x <= W; x += 16) { + d.drawFastVLine(x, BODY_Y, BODY_H, accent()); + delay(4); + d.drawFastVLine(x, BODY_Y, BODY_H, bg()); + } +} } diff --git a/src/core/ui.h b/src/core/ui.h index b5a39bd..f4906ee 100644 --- a/src/core/ui.h +++ b/src/core/ui.h @@ -1,25 +1,40 @@ #pragma once #include +#include "theme.h" -// Tiny drawing helpers shared by all modules. The Cardputer screen is -// 240x135. We reserve a 16px title bar and a 12px hint bar, leaving a -// 240x107 body starting at y=16. +// Drawing + animation helpers. All color comes from the active theme so +// the whole UI restyles instantly when you switch themes. Screen is +// 240x135; 16px title bar, 12px hint bar, 240x107 body at y=16. namespace ui { constexpr int W = 240, H = 135; constexpr int BODY_Y = 16, BODY_H = 107; -// Palette (RGB565 via M5GFX helpers). -inline uint16_t bg() { return M5.Display.color565(12, 14, 18); } -inline uint16_t fg() { return M5.Display.color565(210, 214, 220); } -inline uint16_t accent(){ return M5.Display.color565(80, 200, 120); } -inline uint16_t warn() { return M5.Display.color565(230, 170, 60); } -inline uint16_t bad() { return M5.Display.color565(220, 80, 80); } +uint16_t rgb(uint32_t c); // 0xRRGGBB -> 565 +inline uint16_t bg() { return rgb(theme::active().bg); } +inline uint16_t panel() { return rgb(theme::active().panel); } +inline uint16_t fg() { return rgb(theme::active().fg); } +inline uint16_t dim() { return rgb(theme::active().dim); } +inline uint16_t accent() { return rgb(theme::active().accent); } +inline uint16_t warn() { return rgb(theme::active().warn); } +inline uint16_t bad() { return rgb(theme::active().bad); } +inline uint16_t glow() { return rgb(theme::active().glow); } + +// Interpolate a->b (0xRRGGBB) by t[0..1], return 565. For pulses/fades. +uint16_t mix(uint32_t a, uint32_t b, float t); +// A 0..1 breathing value derived from millis() at the given period(ms). +float pulse(uint32_t periodMs); void titleBar(const char* title, const char* blurb); void hintBar(const char* hint); void clearBody(); -// Print a line at body row `row` (0-based, 12px pitch). void line(int row, const char* fmt, ...); void lineC(int row, uint16_t color, const char* fmt, ...); + +// Animations ------------------------------------------------------- +void bootSplash(); // one-shot boot intro +void wipeIn(); // transition when entering a module +void scanlines(); // subtle CRT overlay on the body +void spinner(int x, int y, uint16_t col); // small animated activity dot +void bar(int row, float frac, uint16_t col, const char* label); // progress bar } diff --git a/src/modules/busdump.cpp b/src/modules/busdump.cpp new file mode 100644 index 0000000..684bf3a --- /dev/null +++ b/src/modules/busdump.cpp @@ -0,0 +1,126 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" +#include +#include +#include + +// Bus Dump — read the contents of memory chips on YOUR boards. +// SPI : 25-series NOR flash. JEDEC ID, then 0x03 READ the whole chip +// to /dump/spi_.bin on microSD. +// I2C : scan the bus, dump a 24-series EEPROM to /dump/i2c_.bin. +// Read-only; never writes the target. [space] start/stop. + +class BusDump : public Module { + enum Mode { SPI_NOR, I2C_EE } mode = SPI_NOR; + bool running = false; + uint32_t addr = 0, size = 0, jedec = 0; + uint8_t i2cAddr = 0x50; + char msg[3][40] = {{0},{0},{0}}; + File out; + bool sdOk = false; + static constexpr int CS = pins::PROBE[2]; +public: + const char* name() const override { return "Bus Dump"; } + const char* blurb() const override { return "SPI/I2C chip readout"; } + + void onEnter() override { + running = false; addr = 0; size = 0; jedec = 0; + sdOk = SD.begin(); + say(sdOk ? "SD ready" : "no SD (dump->serial)"); + } + void onExit() override { if (out) out.close(); running = false; SPI.end(); Wire.end(); } + + bool onKey(char c) override { + if (c == 'm') { mode = mode==SPI_NOR?I2C_EE:SPI_NOR; running=false; say("mode switched"); return true; } + if (c == 'i') { probeId(); return true; } + if (c == 's') { scanI2C(); return true; } + if (c == '+') { i2cAddr = (i2cAddr+1)&0x7F; return true; } + if (c == ' ' || c == '\r') { running ? stop() : start(); return true; } + return false; + } + + void tick() override { + if (!running) return; + uint8_t buf[64]; + uint32_t n = (size-addr) < sizeof(buf) ? (size-addr) : sizeof(buf); + if (n == 0) { stop(); say("-- dump complete --"); return; } + if (mode == SPI_NOR) spiRead(addr, buf, n); else i2cRead(addr, buf, n); + if (out) out.write(buf, n); else dumpSerial(buf, n); + addr += n; + } + + void draw() override { + ui::lineC(0, ui::accent(), "%s %s", mode==SPI_NOR?"SPI-NOR":"I2C-EE", + running?"DUMPING":"idle"); + if (mode == SPI_NOR) ui::line(1, "JEDEC %06lX size %luKB", + (unsigned long)jedec, (unsigned long)(size/1024)); + else ui::line(1, "addr 0x%02X size %luB", i2cAddr, (unsigned long)size); + if (running && size) ui::bar(2, addr/(float)size, ui::glow(), "read"); + for (int i=0;i<3;i++) ui::line(4+i, "%s", msg[i]); + if (running) ui::spinner(228, ui::BODY_Y+1, ui::glow()); + ui::hintBar("[m]ode [i]d [s]can [+]addr [space]go"); + } +private: + void say(const char* fmt, ...) { + for (int i=2;i>0;i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap,fmt); vsnprintf(msg[0],40,fmt,ap); va_end(ap); + } + void spiBegin() { SPI.begin(pins::PROBE[3], pins::PROBE[4], pins::PROBE[5], CS); + pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); } + void probeId() { + if (mode != SPI_NOR) { say("id: SPI mode only"); return; } + spiBegin(); + SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + digitalWrite(CS, LOW); SPI.transfer(0x9F); + uint8_t m=SPI.transfer(0), t=SPI.transfer(0), c=SPI.transfer(0); + digitalWrite(CS, HIGH); SPI.endTransaction(); + jedec = (m<<16)|(t<<8)|c; + size = 1UL << c; + if (c < 16 || c > 26) size = 0; + say("JEDEC %06lX %s", (unsigned long)jedec, size?"":"(unknown cap)"); + } + void spiRead(uint32_t a, uint8_t* b, uint32_t n) { + SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + digitalWrite(CS, LOW); SPI.transfer(0x03); + SPI.transfer((a>>16)&0xFF); SPI.transfer((a>>8)&0xFF); SPI.transfer(a&0xFF); + for (uint32_t i=0;i>8)&0xFF); Wire.write(a&0xFF); + Wire.endTransmission(); + Wire.requestFrom((int)i2cAddr, (int)n); + for (uint32_t i=0;i %s", path); + } else say("-> serial (hex)"); + running = true; + } + void stop() { running = false; if (out) out.close(); } + void dumpSerial(const uint8_t* b, uint32_t n) { + for (uint32_t i=0;i +#include +#include "mbedtls/aes.h" + +// Crypto Lab — offline analysis of a blob you dumped (Bus Dump output on +// the SD card). Helps you tell encrypted from packed from plaintext, and +// try the obvious cheap keys on firmware you own. +// ENTROPY : Shannon bits/byte over the sample (8.0 ~ encrypted/packed). +// IDENT : magic-byte sniff (gzip, ELF, PNG, JFFS2, uImage, ...). +// XOR : single-byte XOR brute; scores printable ratio, shows best. +// AES : AES-128-ECB decrypt with a key in aesKey[] (known-key). +// Loads /dump/*.bin — first file found, or cycle with [f]. + +class Crypto : public Module { + enum Mode { ENTROPY, IDENT, XORB, AESD } mode = ENTROPY; + static constexpr int CAP = 1024; + uint8_t buf[CAP]; int len = 0; + char fname[32] = ""; + char msg[4][40] = {{0},{0},{0},{0}}; + uint8_t aesKey[16] = {0}; bool haveKey = false; +public: + const char* name() const override { return "Crypto Lab"; } + const char* blurb() const override { return "entropy/id/xor/aes"; } + + void onEnter() override { if (!SD.begin()) say("no SD"); else loadFirst(); } + + bool onKey(char c) override { + if (c == 'm') { mode = (Mode)((mode+1)%4); analyse(); return true; } + if (c == 'f') { loadNext(); return true; } + if (c == 'g') { analyse(); return true; } + return false; + } + void draw() override { + const char* mn[] = {"ENTROPY","IDENT","XOR","AES"}; + ui::lineC(0, ui::accent(), "%s file:%s (%dB)", mn[mode], + fname[0]?fname:"none", len); + for (int i=0;i<4;i++) ui::line(2+i, "%s", msg[i]); + ui::hintBar("[m]ode [f]ile [g]o (dumps in /dump)"); + } +private: + void say(const char* fmt, ...) { + for (int i=3;i>0;i--) strncpy(msg[i], msg[i-1], 39); + va_list ap; va_start(ap,fmt); vsnprintf(msg[0],40,fmt,ap); va_end(ap); + } + void clearMsg(){ for(int i=0;i<4;i++) msg[i][0]=0; } + + void loadFrom(File dir, const char* want) { + File f; + while ((f = dir.openNextFile())) { + if (!f.isDirectory()) { + const char* nm = f.name(); + if (!want || strcmp(nm, want) > 0) { + strncpy(fname, nm, 31); + len = f.read(buf, CAP); f.close(); + analyse(); return; + } + } + f.close(); + } + say("no more files"); + } + void loadFirst(){ File d=SD.open("/dump"); if(d){ loadFrom(d, nullptr); d.close(); } else say("mkdir /dump first"); } + void loadNext(){ File d=SD.open("/dump"); if(d){ char cur[32]; strncpy(cur,fname,31); loadFrom(d, cur); d.close(); } } + + void analyse() { + clearMsg(); + if (!len) { say("load a dump first"); return; } + switch (mode) { + case ENTROPY: doEntropy(); break; + case IDENT: doIdent(); break; + case XORB: doXor(); break; + case AESD: doAes(); break; + } + } + void doEntropy() { + int cnt[256] = {0}; + for (int i=0;i7.5 ? "-> encrypted/compressed" : H>5 ? "-> code/packed?" : "-> low: text/config"); + } + void doIdent() { + struct M { int off; const char* sig; int n; const char* name; }; + static const M sigs[] = { + {0,"\x1f\x8b",2,"gzip"}, {0,"\x7f""ELF",4,"ELF"}, + {0,"\x89PNG",4,"PNG"}, {0,"BZh",3,"bzip2"}, + {0,"\x27\x05\x19\x56",4,"uImage"}, {0,"\x85\x19",2,"JFFS2"}, + {0,"hsqs",4,"squashfs"}, {0,"UBI#",4,"UBI"}, + {0,"\xff\xd8\xff",3,"JPEG"}, + }; + for (auto& s : sigs) + if (len>=s.off+s.n && memcmp(buf+s.off, s.sig, s.n)==0) { say("magic: %s", s.name); return; } + say("no known magic (raw/enc?)"); + } + void doXor() { + int bestKey=0; double bestScore=-1; + for (int k=0;k<256;k++) { + int print=0; + for (int i=0;i=32&&v<127) print++; } + double s=(double)print/len; + if (s>bestScore){ bestScore=s; bestKey=k; } + } + say("best XOR key 0x%02X", bestKey); + say("printable %.0f%%", bestScore*100); + char prev[24]; int j=0; + for (int i=0;i<20 && i=32&&v<127)?v:'.'; } + prev[j]=0; say("%s", prev); + } + void doAes() { + say(haveKey ? "AES-128-ECB, custom key" : "AES-128-ECB, key=00..00"); + if (!haveKey) say("(edit aesKey[] for real key)"); + mbedtls_aes_context ctx; mbedtls_aes_init(&ctx); + mbedtls_aes_setkey_dec(&ctx, aesKey, 128); + uint8_t o[16]; + int blocks = len/16 < 2 ? len/16 : 2; + for (int b=0;b +#include "mbedtls/base64.h" + +// Exfil / Export — get captured data OFF the Cardputer for analysis on +// your workstation. Browse the SD card and stream a selected dump out +// over USB serial as base64 (paste into `base64 -d > file` on the host). +// Also writes a session manifest of what you pulled. A data-offload +// helper for your own captures — not a network channel. + +class Exfil : public Module { + static constexpr int MAXF = 32; + char files[MAXF][32]; uint32_t sizes[MAXF]; int nf = 0, sel = 0; + bool sending = false; File cur; uint32_t sent = 0, total = 0; + char status[2][40] = {{0},{0}}; +public: + const char* name() const override { return "Exfil"; } + const char* blurb() const override { return "export dumps->host"; } + + void onEnter() override { sending=false; if (!SD.begin()) say("no SD"); else scan(); } + void onExit() override { if (cur) cur.close(); sending=false; } + + bool onKey(char c) override { + if (sending) { if (c==' ') stop(); return true; } + int m = nf ? nf : 1; + if (c == ';') { sel = (sel-1+m)%m; return true; } + if (c == '.') { sel = (sel+1)%m; return true; } + if (c == 'r') { scan(); return true; } + if (c == '\r' || c == ' ') { startSend(); return true; } + if (c == 'x') { writeManifest(); return true; } + return false; + } + + void tick() override { + if (!sending) return; + uint8_t in[48]; int n = cur.read(in, sizeof(in)); + if (n <= 0) { Serial.println(); say("done: %s", files[sel]); stop(); return; } + unsigned char out[80]; size_t olen = 0; + mbedtls_base64_encode(out, sizeof(out), &olen, in, n); + Serial.write(out, olen); + sent += n; + } + + void draw() override { + ui::lineC(0, ui::accent(), "SD export %s", sending ? "STREAMING" : "browse"); + if (sending) { + ui::bar(1, total ? sent/(float)total : 0, ui::glow(), "b64->usb"); + } else { + for (int i = 0; i < nf && i < 5; i++) + ui::lineC(1+i, i==sel ? ui::glow() : ui::fg(), "%c %-16s %luB", + i==sel?char(0x10):' ', files[i], (unsigned long)sizes[i]); + if (!nf) ui::line(1, "no files in /dump"); + } + for (int i = 0; i < 2; i++) ui::line(6+i, "%s", status[i]); + ui::hintBar("; . pick enter send [x]manifest [`]back"); + } + +private: + void say(const char* fmt, ...) { + strncpy(status[1], status[0], 39); + va_list ap; va_start(ap,fmt); vsnprintf(status[0],40,fmt,ap); va_end(ap); + } + void scan() { + nf = 0; sel = 0; + File d = SD.open("/dump"); + if (!d) { say("no /dump dir"); return; } + File f; + while ((f = d.openNextFile()) && nf < MAXF) { + if (!f.isDirectory()) { + strncpy(files[nf], f.name(), 31); + sizes[nf] = f.size(); + nf++; + } + f.close(); + } + d.close(); + say("%d file(s)", nf); + } + void startSend() { + if (!nf) { say("nothing to send"); return; } + char path[48]; snprintf(path, sizeof(path), "/dump/%s", files[sel]); + cur = SD.open(path, FILE_READ); + if (!cur) { say("open fail"); return; } + total = sizes[sel]; sent = 0; sending = true; + Serial.printf("\n----- BEGIN %s (base64) -----\n", files[sel]); + say("streaming %s", files[sel]); + } + void stop() { + if (cur) cur.close(); + Serial.printf("\n----- END -----\n"); + sending = false; + } + void writeManifest() { + File m = SD.open("/dump/MANIFEST.txt", FILE_WRITE); + if (!m) { say("manifest fail"); return; } + m.printf("Card-Crack export manifest\n"); + for (int i = 0; i < nf; i++) m.printf("%-20s %lu B\n", files[i], (unsigned long)sizes[i]); + m.close(); + say("wrote MANIFEST.txt"); + } +}; + +Module* makeExfil() { return new Exfil(); } diff --git a/src/modules/themepick.cpp b/src/modules/themepick.cpp new file mode 100644 index 0000000..5e93e00 --- /dev/null +++ b/src/modules/themepick.cpp @@ -0,0 +1,43 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/theme.h" +#include + +// Live theme switcher. Preview swatches, [space]/arrows to cycle, choice +// saved to NVS so it sticks across reboots. +class ThemePicker : public Module { +public: + const char* name() const override { return "Theme"; } + const char* blurb() const override { return "restyle the UI"; } + + bool onKey(char c) override { + if (c == '.' || c == ' ') { theme::set(theme::current() + 1); repaint(); return true; } + if (c == ';') { theme::set(theme::current() - 1); repaint(); return true; } + return false; + } + void onEnter() override { repaint(); } + + void draw() override { + auto& d = M5.Display; + ui::lineC(0, ui::accent(), "theme: %s (%d/%d)", + theme::active().name, theme::current() + 1, theme::COUNT); + // swatch row + int y = ui::BODY_Y + 20; + for (int i = 0; i < theme::COUNT; i++) { + const Theme& t = theme::LIST[i]; + int x = 6 + i * 30; + d.fillRoundRect(x, y, 24, 24, 4, ui::rgb(t.bg)); + d.fillRect(x + 4, y + 4, 16, 5, ui::rgb(t.accent)); + d.fillRect(x + 4, y + 11, 10, 4, ui::rgb(t.warn)); + d.fillRect(x + 4, y + 16, 14, 4, ui::rgb(t.bad)); + d.drawRoundRect(x, y, 24, 24, 4, i == theme::current() ? ui::glow() : ui::dim()); + } + ui::line(5, "everything restyles instantly."); + ui::hintBar("; / . cycle [`]back"); + } +private: + void repaint() { M5.Display.fillScreen(ui::bg()); + ui::titleBar(name(), blurb()); draw(); } +}; + +Module* makeThemePicker() { return new ThemePicker(); }