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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAhMHMRAQLQ9hSbBECKNfn
This commit is contained in:
Indiana Holmes
2026-09-11 00:17:57 +00:00
parent 79612197a1
commit d1c37b83e0
10 changed files with 615 additions and 42 deletions

View File

@@ -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

View File

@@ -1,39 +1,56 @@
#include "shell.h"
#include "ui.h"
#include "theme.h"
#include <M5Cardputer.h>
// 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();

33
src/core/theme.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "theme.h"
#include <Preferences.h>
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;
}
}

20
src/core/theme.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <stdint.h>
// 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
}

View File

@@ -1,53 +1,125 @@
#include "ui.h"
#include <stdarg.h>
#include <math.h>
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());
}
}
}

View File

@@ -1,25 +1,40 @@
#pragma once
#include <M5Cardputer.h>
#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
}

126
src/modules/busdump.cpp Normal file
View File

@@ -0,0 +1,126 @@
#include "../core/module.h"
#include "../core/ui.h"
#include "../core/pins.h"
#include <SPI.h>
#include <Wire.h>
#include <SD.h>
// 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_<id>.bin on microSD.
// I2C : scan the bus, dump a 24-series EEPROM to /dump/i2c_<addr>.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<n;i++) b[i]=SPI.transfer(0);
digitalWrite(CS, HIGH); SPI.endTransaction();
}
void scanI2C() {
Wire.begin(pins::GROVE_A, pins::GROVE_B);
int found = 0;
for (uint8_t a=0x08; a<0x78; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission()==0) { i2cAddr=a; found++; }
}
say("i2c: %d dev, sel 0x%02X", found, i2cAddr);
}
void i2cRead(uint32_t a, uint8_t* b, uint32_t n) {
Wire.beginTransmission(i2cAddr);
Wire.write((a>>8)&0xFF); Wire.write(a&0xFF);
Wire.endTransmission();
Wire.requestFrom((int)i2cAddr, (int)n);
for (uint32_t i=0;i<n && Wire.available();i++) b[i]=Wire.read();
}
void start() {
if (mode == SPI_NOR) { probeId(); if (!size) { say("probe chip first"); return; } }
else { Wire.begin(pins::GROVE_A, pins::GROVE_B); size = 8192; }
addr = 0;
if (sdOk) {
SD.mkdir("/dump");
char path[40];
if (mode==SPI_NOR) snprintf(path,sizeof(path),"/dump/spi_%06lX.bin",(unsigned long)jedec);
else snprintf(path,sizeof(path),"/dump/i2c_%02X.bin", i2cAddr);
out = SD.open(path, FILE_WRITE);
say("-> %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<n;i++) Serial.printf("%02X", b[i]);
}
};
Module* makeBusDump() { return new BusDump(); }

129
src/modules/crypto.cpp Normal file
View File

@@ -0,0 +1,129 @@
#include "../core/module.h"
#include "../core/ui.h"
#include <SD.h>
#include <math.h>
#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;i<len;i++) cnt[buf[i]]++;
double H = 0;
for (int i=0;i<256;i++) if (cnt[i]) { double p=(double)cnt[i]/len; H -= p*log2(p); }
say("Shannon: %.2f bits/byte", H);
say(H>7.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<len;i++){ uint8_t v=buf[i]^k; if(v>=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<len;i++){ uint8_t v=buf[i]^bestKey; prev[j++]=(v>=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<blocks;b++) {
mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_DECRYPT, buf+b*16, o);
char h[40]; int p=0;
for (int i=0;i<16;i++) p+=snprintf(h+p,sizeof(h)-p,"%02X",o[i]);
say("%s", h);
}
mbedtls_aes_free(&ctx);
}
};
Module* makeCrypto() { return new Crypto(); }

104
src/modules/exfil.cpp Normal file
View File

@@ -0,0 +1,104 @@
#include "../core/module.h"
#include "../core/ui.h"
#include <SD.h>
#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(); }

43
src/modules/themepick.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include "../core/module.h"
#include "../core/ui.h"
#include "../core/theme.h"
#include <M5Cardputer.h>
// 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(); }