把任意文件打包隐写到 32-bit BMP 像素里,一张图带走。加密、流式、纯前端。支持分卷编码突破单 BMP 文件大小限制。
- 分卷编码 索引分卷包含文件条目 + 部分数据,数据分卷只有数据,多 BMP 无缝拼接
- 加密打包 AES-256-CTR 加密内容,PBKDF2 密钥派生,密码校验加密 magic
- 超大文件支持 单文件上限 2⁵³ − 1,文件数上限 2³²,总 payload 不限
- 128-bit 完整 counter AES-CTR 使用 128-bit counter,非 short counter,nonce 永不碰撞,大文件无溢出风险
- BGRA 原生编码 数据字节按像素的 BGRA 顺序直接写入,零通道映射开销
- 32-bit BMP 直写 每像素 4 字节,Alpha 通道承载数据,密度比 24-bit 高 33%
- 零填充浪费 像素区取最大完全平方数 k²(k²×4 ≤ payload),尾部数据以 BMP 额外数据存放,bfSize 停在像素区边界,F2P 自读尾巴
- 流式处理 Service Worker 拦截 GET /file/,ReadableStream 分块推送,浏览器直写磁盘
- 顺序编码协调器 分卷逐个编码,内存峰值可控,无每分卷子任务
- 任务历史 编码/解码完成后保留记录,进度条定格 100%,可追溯近期任务
- 拖拽排序 编码区文件列表支持拖拽调整顺序,解码顺序与编码一致
- Tab 状态保持 当前选中的编解码/任务标签页在 session 内持久化,刷新页面后自动恢复
- 编解码分离 顶部标签切换,互不干扰
- 纯前端 所有操作在浏览器完成,不上传服务器
- Service Worker 指示灯 页脚实时显示 SW 生命周期状态,七色联动文字变色
BMP 规格:32-bit BGRA,每像素 4 字节,行步长 = 宽度 × 4。数据直接按 BGRA 原生顺序写入,字节 N → 像素偏移 N(B=0, G=1, R=2, A=3),4 字节对齐的块可直接 Uint8Array.set() 拷贝。
BMP header(54 字节)中的三个关键字段:
bfSize(offset 2)=54 + k²×4,BMP 规范值,只算 header + 像素区biSizeImage(offset 34)=k²×4,像素区大小- 实际文件/流的大小 =
54 + 8 + payloadSize(含尾巴),靠 Content-Length 透传
像素边长 k = floor(sqrt(floor((8 + payloadSize) / 4))),即满足 k²×4 ≤ 8 + payloadSize 的最大完全平方数的平方根。像素区被数据完全填满,没有零填充。剩下的 (8 + payloadSize) − k²×4 字节紧跟在像素区后面,解码器越过 bfSize 直接读。
明文头(36 字节):
[4B magic = 0x46325036 "F2P6"]
[4B encMagic] ← AES-CTR("F2P6", key, segSalt, 0, 128) 前 4B
[8B segID=0]
[16B segSalt] ← PBKDF2 salt + AES-CTR counter base
[4B iter] ← PBKDF2 迭代次数(默认 10000)
加密区(AES-CTR blockOff=1):
[16B indexSalt] ← 用于加密数据分卷 segSalt
[8B segCount] ← 总分卷数
[8B fileCount] ← 文件总数
[fileEntries...] ← 文件条目
[data...] ← 该分卷承载的文件数据
文件条目(与 data 共享同一 AES-CTR 流):
[2B nameLen][8B dataLen][nB name(UTF-8)] 重复 fileCount 次
明文头(32 字节):
[4B magic = 0x46325036 "F2P6"]
[4B encMagic] ← AES-CTR("F2P6", key, segSalt, 0, 128) 前 4B
[8B segID=N]
[16B encryptedSegSalt] ← AES-CTR(segSalt, key, indexSalt, 0, 128)
加密区(AES-CTR blockOff=1):
[data...] ← chunked 加密写入,64KB 块
索引分卷:
encMagic: AES-CTR("F2P6", key, segSalt, 0, 128) → 取前 4B
加密区: AES-CTR(明文, key, segSalt, 1, 128) → 全部加密
数据分卷:
encryptedSegSalt: AES-CTR(segSalt, key, indexSalt, 0, 128)
encMagic: AES-CTR("F2P6", key, segSalt, 0, 128) → 取前 4B
数据区: AES-CTR(原文, key, segSalt, 1, 128) → chunked
解码器自动识别 F2P6 / F2P5 / F2P4 / F2P3 / F2P2 / F2P1:
| 版本 | 魔数 | BMP 位深 | 通道映射 | Counter 宽度 | 备注 |
|---|---|---|---|---|---|
| F2P6 | 0x46325036 |
32-bit | BGRA 原生 | 128-bit | 分卷编码 |
| F2P5 | 0x46325035 |
32-bit | BGRA 原生 | 128-bit | 单 BMP |
| F2P4 | 0x46325034 |
32-bit | BGRA 原生 | short | |
| F2P3 | 0x46325033 |
32-bit | [2,1,0,3] 映射 |
short | |
| F2P2 | 0x46325032 |
24-bit | [2,1,0] 映射 |
short | |
| F2P1 | 0x46325031 |
24-bit | [2,1,0] 映射 |
无加密 |
编解码在 Service Worker 中执行,页面仅负责 UI 交互。下载通过 SW fetch 拦截 + 302 重定向 + ReadableStream 直出。
编码区点击生成 → postMessage encode → SW 预计算分卷布局
→ 回复 encode-ready(含分卷数)
→ triggerDownload("/files?id=<jobId>&idx=0") (× segCount)
解码区点击下载 → postMessage f2p6-decode-group
→ SW 存储分组信息
→ triggerDownload("/files?id=<gid>&idx=<n>")
下载触发链路:
页面: 创建隐藏 iframe,src = /files?id=<xxx>[&idx=<n>]
SW: 拦截 GET /files
→ 查 pendingEncodeGroups / pendingDecodeGroups / pendingF2P6DecodeGroups
→ 派生 hash = SHA1(id[+idx])
→ 注册 fileRoutes[hash] = { id, idx, kind }
→ 302 → /file/<hash>/<filename>
页面: iframe 跟随 302
SW: 拦截 GET /file/<hash>/<filename>
→ 查 fileRoutes,清除条目
→ 创建 ReadableStream 分块推送
→ 设置 Content-Disposition: attachment + Content-Length
→ 浏览器接收流式写入磁盘
批量下载逐个触发 triggerDownload,各 iframe 独立导航互不抢占。
SW 不缓存完整文件,逐 chunk 读写,内存占用稳定在 chunkSize × 8 左右。
编码使用 precomputeSegments 预计算分卷布局,然后通过 encodeSegmentsSequentially 顺序协调器逐个编码。每个分卷通过 ReadableStream 推送 BMP 头部 + 像素行 + 尾巴,Chunked AES-CTR 保持计数器连续。
src/
├── index.html ← 页面骨架
├── main.js ← 入口,挂载 Vue 应用并注册 SW
├── style.css ← 暗色主题样式
├── App.vue ← 根组件:布局 + 标签切换
├── assets/
│ ├── favicon.svg ← 站点图标
│ └── manifest.json ← PWA manifest
├── components/
│ ├── TopBar.vue ← 顶部标签栏 + ChunkSize 选择器
│ ├── EncodePanel.vue← 编码面板:文件拖放、密码、分卷大小、提交
│ ├── DecodePanel.vue← 解码面板:BMP 选择、密码、解码触发
│ ├── TasksPanel.vue ← 任务列表:实时进度 + 历史记录
│ ├── ToastHost.vue ← Toast 通知容器
│ └── SWStatus.vue ← Service Worker 状态指示灯
├── composables/
│ └── useSettings.js ← localStorage 设置持久化
├── sw.js ← Service Worker:编解码执行 + 流式下载拦截 + PWA 缓存 + 顺序协调器
└── lib/
├── sw-client.js ← SW 通信层(消息投递、Toast、triggerDownload)
├── f2p-core.js ← 核心库(加密工具、BMP 构建/读取、魔数常量)
├── f2p-encode.js ← 编码入口,代理到 F2P6 编码器
├── f2p-decode.js ← 解码入口,自动识别版本派发
└── coders/
├── f2p6-encode.js ← F2P6 编码器(precomputeSegments、chunked 加密、进度/取消)
├── f2p6-decode.js ← F2P6 解码器(extractFileData/DataRange、流式提取)
├── f2p5-encode.js ← F2P5 编码器(保留,未使用)
├── f2p5-decode.js ← F2P5 解码器
├── f2p4-decode.js ← F2P4 解码器
├── f2p3-decode.js ← F2P3 解码器
├── f2p2-decode.js ← F2P2 解码器
└── f2p1-decode.js ← F2P1 解码器
npm install
npm run build # 生产构建 → dist/
npm run dev # 开发 → http://localhost:3000- 编码:拖放文件 → 可选密码 → 可选分卷大小 → 拖拽排序 → 生成图片
- 解码:拖放 BMP(支持多选)→ 输入密码 → 解码分卷 → 点选或批量下载
- 任务:实时进度条,完成后保留记录供查阅
- 留空 = 空字符串密钥(每份 BMP 有随机 salt,密文每次都不同)
- 设密码 = 自定义密钥
- 解码时密码错误会被 encMagic 校验捕获
- F2P6 强制加密所有内容(文件条目 + 数据)
编码区新增分卷下拉菜单。选 0(默认)不分卷,全部数据装入单个 BMP。选 10MB+ 时自动切分,每张 BMP 不超过目标大小。解码时只需将所有 BMP 一起拖入解码区,解码器自动识别索引分卷和数据分卷。
顶部栏可调,编解码共用。大块(4 MB+)适合高性能设备,小块(64 KB 以下)兼容低端设备。默认 64 KB。选择值自动持久化到 localStorage。
npm install
npm run build # Rspack + html-minifier-terser产物在 dist/,通过任意静态托管部署。