Skip to content

Commit 401dd48

Browse files
bod09claude
andcommitted
Add hex editor, security analysis, ZIP download, and file management features
Round 2 feature batch: full hex editor with byte-level editing, SHA-256 file hashing (firmware + web), entropy analysis for encrypted/packed file detection, EXIF metadata viewer and stripping, recursive security scan with exportable reports, multi-file ZIP download, clipboard paste upload, recursive directory delete, drive formatting, grid/list view toggle, and keyboard shortcuts for file navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4c1079e commit 401dd48

19 files changed

Lines changed: 1367 additions & 82 deletions

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ Built for incident response, security research, air-gapped environments, and any
1313
| **Autorun / malware execution** | Drive is never mounted by your PC — no autorun, no shell extensions, no thumbnail handlers |
1414
| **Filesystem driver exploits** | Your OS filesystem drivers (NTFS, exFAT, etc.) never touch the drive. FatFS on the RP2350 parses the filesystem in an isolated environment |
1515
| **Malicious USB device attacks** | The drive connects to the RP2350's USB host port, not your PC. Your PC only sees a standard HID keyboard + CDC serial device |
16-
| **Content inspection** | Built-in file preview (text, images, audio, video, PDF) lets you examine contents without downloading. Magic bytes mismatch detection flags files where the extension doesn't match the actual content |
17-
| **Suspicious file detection** | Known dangerous extensions (.exe, .bat, .ps1, .vbs, .scr, autorun.inf, etc.) are flagged with warning icons. Security scan summarizes threats in the current directory |
16+
| **Content inspection** | Built-in file preview (text, images, audio, video, PDF, hex editor) lets you examine contents without downloading. Magic bytes mismatch detection flags files where the extension doesn't match the actual content |
17+
| **Suspicious file detection** | Known dangerous extensions (.exe, .bat, .ps1, .vbs, .scr, autorun.inf, etc.) are flagged with warning icons. Recursive security scan with exportable reports |
18+
| **Entropy analysis** | Shannon entropy calculation flags encrypted, compressed, or packed files that may be obfuscated malware |
19+
| **File hashing** | On-device SHA-256 hashing — verify file integrity without downloading |
20+
| **EXIF metadata** | View and strip EXIF metadata from JPEG images — remove GPS coordinates, camera info, and other identifying data before downloading |
1821
| **Browser sandbox** | All file rendering happens inside the browser's sandboxed environment — even if you preview a malicious file, it can't escape the browser sandbox |
1922

2023
### Limitations
@@ -26,10 +29,17 @@ Built for incident response, security research, air-gapped environments, and any
2629

2730
## Features
2831

29-
- **File browser** — Navigate directories, upload, download, rename, delete files on the USB drive
30-
- **File preview & edit** — Open files directly in the browser: text (with editing), images, audio, video, PDF, and hex dump for unknown formats
32+
- **File browser** — Navigate directories with list or grid view, upload, download, rename, delete files
33+
- **File preview & edit** — Text editor, image viewer with zoom, audio/video player, PDF viewer, full hex editor with byte-level editing
34+
- **Multi-file ZIP download** — Select multiple files and download as a single ZIP archive
35+
- **Clipboard paste upload** — Paste images or files from clipboard directly into the file browser
3136
- **Folder sizes** — Directories show total recursive size (calculated on-device)
32-
- **Security scanning** — Warning icons on suspicious files, magic bytes mismatch detection, one-click threat scan
37+
- **SHA-256 hashing** — Compute file hashes on-device for integrity verification
38+
- **Security scanning** — Recursive threat scan with warning icons, entropy analysis, magic bytes mismatch detection, exportable scan reports
39+
- **EXIF viewer & stripper** — Inspect and remove EXIF metadata from JPEG images
40+
- **Recursive delete** — Delete directories and all their contents in one operation
41+
- **Format drive** — Reformat the drive from the settings page
42+
- **Keyboard shortcuts** — Arrow keys, Enter, Delete, Ctrl+A, Backspace navigation
3343
- **Air-gap indicator** — Visual confirmation that the drive is accessed through the RP2350, not directly by your PC
3444
- **PWA support** — Installable as a standalone app with offline caching
3545

@@ -122,6 +132,9 @@ Newline-delimited JSON over CDC serial. Commands:
122132
| `{"cmd":"delete","path":"/file.txt"}` | Delete file/directory |
123133
| `{"cmd":"rename","from":"/old","to":"/new"}` | Rename/move |
124134
| `{"cmd":"dirsize","path":"/dir"}` | Recursive directory size |
135+
| `{"cmd":"hash","path":"/file.txt"}` | SHA-256 file hash |
136+
| `{"cmd":"rmdir","path":"/dir"}` | Recursive directory delete |
137+
| `{"cmd":"format"}` | Format drive |
125138
| `{"cmd":"df"}` | Disk free space |
126139
| `{"cmd":"eject"}` | Safe unmount |
127140
| `{"cmd":"status"}` | Drive status |

firmware/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ add_executable(storage_bridge
5050
src/msc_diskio.c
5151
src/file_ops.c
5252
src/base64.c
53+
src/sha256.c
5354
# FatFS core — compiled from build dir copy that has our ffconf.h
5455
${FATFS_BUILD_DIR}/ff.c
5556
${FATFS_BUILD_DIR}/ffunicode.c

firmware/src/ffconf.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#define FF_FS_READONLY 0 // Read-write
1212
#define FF_FS_MINIMIZE 0 // Full API
1313
#define FF_USE_FIND 1 // f_findfirst / f_findnext
14-
#define FF_USE_MKFS 0 // No formatting support
14+
#define FF_USE_MKFS 1 // Enable formatting support
1515
#define FF_USE_FASTSEEK 0
1616
#define FF_USE_EXPAND 0
1717
#define FF_USE_CHMOD 0

firmware/src/file_ops.c

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "serial_cmd.h"
33
#include "base64.h"
44
#include "msc_host.h"
5+
#include "sha256.h"
56
#include "ff.h"
67
#include "tusb.h"
78
#include "hardware/watchdog.h"
@@ -298,3 +299,139 @@ void file_op_dirsize(const char* path) {
298299
path, total_size, file_count, dir_count);
299300
cdc_write_chunked(buf, len);
300301
}
302+
303+
void file_op_hash(const char* path) {
304+
FIL fil;
305+
FRESULT res = f_open(&fil, path, FA_READ);
306+
if (res != FR_OK) { send_error(res); return; }
307+
308+
sha256_ctx ctx;
309+
sha256_init(&ctx);
310+
311+
uint32_t fsize = f_size(&fil);
312+
uint32_t remaining = fsize;
313+
uint32_t tick = 0;
314+
315+
while (remaining > 0) {
316+
uint32_t chunk = remaining > FILE_CHUNK_SIZE ? FILE_CHUNK_SIZE : remaining;
317+
UINT bytes_read = 0;
318+
res = f_read(&fil, file_buf, chunk, &bytes_read);
319+
if (res != FR_OK) { f_close(&fil); send_error(res); return; }
320+
sha256_update(&ctx, file_buf, bytes_read);
321+
remaining -= bytes_read;
322+
323+
if (++tick % 4 == 0) {
324+
watchdog_update();
325+
tud_task();
326+
tuh_task();
327+
}
328+
}
329+
330+
f_close(&fil);
331+
332+
uint8_t hash[32];
333+
sha256_final(&ctx, hash);
334+
335+
char hex[65];
336+
for (int i = 0; i < 32; i++) {
337+
hex[i * 2] = "0123456789abcdef"[hash[i] >> 4];
338+
hex[i * 2 + 1] = "0123456789abcdef"[hash[i] & 0x0F];
339+
}
340+
hex[64] = '\0';
341+
342+
char buf[384];
343+
int len = snprintf(buf, sizeof(buf),
344+
"{\"type\":\"hash\",\"path\":\"%s\",\"sha256\":\"%s\","
345+
"\"size\":%" PRIu32 "}\n",
346+
path, hex, fsize);
347+
cdc_write_chunked(buf, len);
348+
}
349+
350+
#define RMRF_MAX_DEPTH 8
351+
352+
void file_op_delete_recursive(const char* path) {
353+
// First try simple unlink (works for files and empty dirs)
354+
FRESULT res = f_unlink(path);
355+
if (res == FR_OK) {
356+
cdc_send("{\"status\":\"ok\"}\n");
357+
return;
358+
}
359+
360+
// Must be a non-empty directory — walk and delete bottom-up
361+
DIR dir_stack[RMRF_MAX_DEPTH];
362+
// Phase flags: 0 = scanning entries, 1 = done scanning (ready to delete dir)
363+
uint8_t phase[RMRF_MAX_DEPTH];
364+
int depth = 0;
365+
FILINFO fno;
366+
char pathbuf[256];
367+
uint32_t tick = 0;
368+
369+
strncpy(pathbuf, path, sizeof(pathbuf) - 1);
370+
pathbuf[sizeof(pathbuf) - 1] = '\0';
371+
372+
res = f_opendir(&dir_stack[0], pathbuf);
373+
if (res != FR_OK) { send_error(res); return; }
374+
phase[0] = 0;
375+
376+
while (depth >= 0) {
377+
if (++tick % 20 == 0) {
378+
watchdog_update();
379+
tud_task();
380+
tuh_task();
381+
}
382+
383+
res = f_readdir(&dir_stack[depth], &fno);
384+
if (res != FR_OK || fno.fname[0] == '\0') {
385+
// End of directory — close and delete the now-empty dir
386+
f_closedir(&dir_stack[depth]);
387+
if (depth > 0) {
388+
f_unlink(pathbuf);
389+
char* slash = strrchr(pathbuf, '/');
390+
if (slash && slash != pathbuf) *slash = '\0';
391+
else if (slash == pathbuf) pathbuf[1] = '\0';
392+
}
393+
depth--;
394+
continue;
395+
}
396+
397+
if (fno.fname[0] == '.') continue;
398+
399+
// Build full path for this entry
400+
size_t plen = strlen(pathbuf);
401+
bool needs_slash = (plen > 0 && pathbuf[plen - 1] != '/');
402+
int written = snprintf(pathbuf + plen, sizeof(pathbuf) - plen,
403+
"%s%s", needs_slash ? "/" : "", fno.fname);
404+
if (plen + written >= sizeof(pathbuf)) {
405+
pathbuf[plen] = '\0';
406+
continue; // path too long, skip
407+
}
408+
409+
if (fno.fattrib & AM_DIR) {
410+
if (depth + 1 < RMRF_MAX_DEPTH) {
411+
depth++;
412+
res = f_opendir(&dir_stack[depth], pathbuf);
413+
if (res != FR_OK) {
414+
pathbuf[plen] = '\0';
415+
depth--;
416+
}
417+
phase[depth] = 0;
418+
} else {
419+
pathbuf[plen] = '\0'; // too deep
420+
}
421+
} else {
422+
f_unlink(pathbuf);
423+
pathbuf[plen] = '\0';
424+
}
425+
}
426+
427+
// Delete the root directory itself
428+
f_unlink(path);
429+
cdc_send("{\"status\":\"ok\"}\n");
430+
}
431+
432+
void file_op_format(void) {
433+
BYTE work[FF_MAX_SS];
434+
FRESULT res = f_mkfs("", FM_ANY, 0, work, sizeof(work));
435+
if (res != FR_OK) { send_error(res); return; }
436+
cdc_send("{\"status\":\"ok\"}\n");
437+
}

firmware/src/file_ops.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ void file_op_write(const char* path, uint32_t offset,
1717
const uint8_t* data, uint32_t data_len, bool done);
1818
void file_op_df(void);
1919
void file_op_dirsize(const char* path);
20+
void file_op_hash(const char* path);
21+
void file_op_delete_recursive(const char* path);
22+
void file_op_format(void);
2023

2124
#endif // FILE_OPS_H

firmware/src/serial_cmd.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ static void process_command(const char* cmd) {
205205
json_get_string(cmd, "path", path, sizeof(path));
206206
file_op_dirsize(path);
207207
}
208+
else if (strstr(cmd, "\"cmd\":\"hash\"")) {
209+
if (!info->mounted) { cdc_send("{\"status\":\"error\",\"msg\":\"No drive\"}\n"); return; }
210+
if (!json_get_string(cmd, "path", path, sizeof(path))) {
211+
cdc_send("{\"status\":\"error\",\"msg\":\"Missing path\"}\n"); return;
212+
}
213+
file_op_hash(path);
214+
}
215+
else if (strstr(cmd, "\"cmd\":\"rmdir\"")) {
216+
if (!info->mounted) { cdc_send("{\"status\":\"error\",\"msg\":\"No drive\"}\n"); return; }
217+
if (!json_get_string(cmd, "path", path, sizeof(path))) {
218+
cdc_send("{\"status\":\"error\",\"msg\":\"Missing path\"}\n"); return;
219+
}
220+
file_op_delete_recursive(path);
221+
}
222+
else if (strstr(cmd, "\"cmd\":\"format\"")) {
223+
if (!info->mounted) { cdc_send("{\"status\":\"error\",\"msg\":\"No drive\"}\n"); return; }
224+
file_op_format();
225+
}
208226
else if (strstr(cmd, "\"cmd\":\"df\"")) {
209227
file_op_df();
210228
}

firmware/src/sha256.c

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#include "sha256.h"
2+
#include <string.h>
3+
4+
static const uint32_t K[64] = {
5+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
6+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
7+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
8+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
9+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
10+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
11+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
12+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
13+
};
14+
15+
#define ROR(x,n) (((x)>>(n))|((x)<<(32-(n))))
16+
#define CH(x,y,z) (((x)&(y))^(~(x)&(z)))
17+
#define MAJ(x,y,z) (((x)&(y))^((x)&(z))^((y)&(z)))
18+
#define EP0(x) (ROR(x,2)^ROR(x,13)^ROR(x,22))
19+
#define EP1(x) (ROR(x,6)^ROR(x,11)^ROR(x,25))
20+
#define SIG0(x) (ROR(x,7)^ROR(x,18)^((x)>>3))
21+
#define SIG1(x) (ROR(x,17)^ROR(x,19)^((x)>>10))
22+
23+
static void sha256_transform(sha256_ctx* ctx, const uint8_t block[64]) {
24+
uint32_t w[64], a, b, c, d, e, f, g, h, t1, t2;
25+
26+
for (int i = 0; i < 16; i++)
27+
w[i] = ((uint32_t)block[i*4]<<24)|((uint32_t)block[i*4+1]<<16)|
28+
((uint32_t)block[i*4+2]<<8)|block[i*4+3];
29+
for (int i = 16; i < 64; i++)
30+
w[i] = SIG1(w[i-2]) + w[i-7] + SIG0(w[i-15]) + w[i-16];
31+
32+
a=ctx->state[0]; b=ctx->state[1]; c=ctx->state[2]; d=ctx->state[3];
33+
e=ctx->state[4]; f=ctx->state[5]; g=ctx->state[6]; h=ctx->state[7];
34+
35+
for (int i = 0; i < 64; i++) {
36+
t1 = h + EP1(e) + CH(e,f,g) + K[i] + w[i];
37+
t2 = EP0(a) + MAJ(a,b,c);
38+
h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2;
39+
}
40+
41+
ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d;
42+
ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h;
43+
}
44+
45+
void sha256_init(sha256_ctx* ctx) {
46+
ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85;
47+
ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a;
48+
ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c;
49+
ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19;
50+
ctx->count = 0;
51+
memset(ctx->buf, 0, 64);
52+
}
53+
54+
void sha256_update(sha256_ctx* ctx, const uint8_t* data, size_t len) {
55+
size_t i = 0;
56+
size_t idx = (size_t)(ctx->count & 63);
57+
ctx->count += len;
58+
59+
if (idx) {
60+
size_t rem = 64 - idx;
61+
if (len < rem) { memcpy(ctx->buf + idx, data, len); return; }
62+
memcpy(ctx->buf + idx, data, rem);
63+
sha256_transform(ctx, ctx->buf);
64+
i = rem;
65+
}
66+
for (; i + 64 <= len; i += 64)
67+
sha256_transform(ctx, data + i);
68+
if (i < len)
69+
memcpy(ctx->buf, data + i, len - i);
70+
}
71+
72+
void sha256_final(sha256_ctx* ctx, uint8_t hash[32]) {
73+
size_t idx = (size_t)(ctx->count & 63);
74+
ctx->buf[idx++] = 0x80;
75+
if (idx > 56) {
76+
memset(ctx->buf + idx, 0, 64 - idx);
77+
sha256_transform(ctx, ctx->buf);
78+
idx = 0;
79+
}
80+
memset(ctx->buf + idx, 0, 56 - idx);
81+
uint64_t bits = ctx->count * 8;
82+
for (int i = 0; i < 8; i++)
83+
ctx->buf[56 + i] = (uint8_t)(bits >> (56 - i * 8));
84+
sha256_transform(ctx, ctx->buf);
85+
for (int i = 0; i < 8; i++) {
86+
hash[i*4] = (uint8_t)(ctx->state[i]>>24);
87+
hash[i*4+1] = (uint8_t)(ctx->state[i]>>16);
88+
hash[i*4+2] = (uint8_t)(ctx->state[i]>>8);
89+
hash[i*4+3] = (uint8_t)(ctx->state[i]);
90+
}
91+
}

firmware/src/sha256.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef SHA256_H
2+
#define SHA256_H
3+
4+
#include <stdint.h>
5+
#include <stddef.h>
6+
7+
typedef struct {
8+
uint32_t state[8];
9+
uint64_t count;
10+
uint8_t buf[64];
11+
} sha256_ctx;
12+
13+
void sha256_init(sha256_ctx* ctx);
14+
void sha256_update(sha256_ctx* ctx, const uint8_t* data, size_t len);
15+
void sha256_final(sha256_ctx* ctx, uint8_t hash[32]);
16+
17+
#endif

web/css/components.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
font-size: 13px;
5959
color: var(--text-secondary);
6060
margin-bottom: 16px;
61+
white-space: pre-wrap;
62+
max-height: 50vh;
63+
overflow-y: auto;
6164
}
6265

6366
.modal-content .input-group {
@@ -210,5 +213,15 @@
210213
margin-top: 1px;
211214
}
212215

216+
/* Danger button */
217+
.danger {
218+
color: var(--error) !important;
219+
border-color: var(--error) !important;
220+
}
221+
222+
.danger:hover {
223+
background: rgba(255, 59, 48, 0.1) !important;
224+
}
225+
213226
/* Hidden file input */
214227
#uploadInput { display: none; }

0 commit comments

Comments
 (0)