dotCooCoo/hermitstash
GitHub: dotCooCoo/hermitstash
一个后量子加密、零明文、私有的自托管文件分享平台,解决数据在传输与存储中的安全性问题。
Stars: 0 | Forks: 0

HermitStash
Stash it quietly. Share it instantly.
Post-quantum encrypted, self-hosted file upload server.
HermitStash Sync — companion desktop sync client
## 快速开始
```
git clone https://github.com/dotCooCoo/hermitstash.git
cd hermitstash
node server.js
```
无需配置文件。无需构建步骤。无需 `npm install` —— 所有依赖项均以零 npm 运行时包的形式寄存在仓库中。首次运行会生成保管库密钥对并创建默认账户。所有配置均通过管理面板完成。
**默认管理员:** `admin@hermitstash.com` / `admin` — 请立即通过设置向导更改。向导会引导你完成管理员邮箱和密码的修改、设置站点名称、配置基于令牌的依赖方(rpOrigin/rpId)以及生成会话密钥。完成后,可通过登录页面使用密码重置功能。
## 为何选择 HermitStash?
- **后量子加密** — 你的文件同时抵御当今计算机与未来量子计算机
- **零明文** — 每个数据库字段、每个文件、每条审计日志在接触磁盘前均被加密或哈希
- **自托管** — 你的服务器、你的密钥、你的数据。不依赖任何第三方云
- **运行时零依赖** — `node server.js` 即为全部设置。所有加密库均已寄存,无需 npm 运行时包
- **一键部署** — Docker 或裸金属,无需构建步骤,无需配置文件
## 加密套件
所有加密操作使用 NIST 标准化的后量子算法:
| 层 | 算法 | 标准 | 用途 |
|----|------|------|------|
| **KEM** | ML-KEM-1024 + P-384 ECDH 混合 | FIPS 203 + NIST P-384 | 密钥封装(PQC + 经典) |
| **对称加密** | XChaCha20-Poly1305 | RFC 8439 扩展 | 数据加密(192 位随机数,常数时间) |
| **KDF** | SHAKE256 | FIPS 202(XOF) | 从 KEM 共享密钥派生密钥 |
| **哈希** | SHA3-512 | FIPS 202 | 完整性、电子邮件/IP 哈希、校验和 |
| **HMAC** | HMAC-SHA3-512 | FIPS 202 | Webhook 签名、令牌验证 |
| **密码** | Argon2id | RFC 9106 | 内存硬密码哈希 |
| **签名** | ML-DSA-87 / SLH-DSA-SHAKE-256f | FIPS 204 / 205 | 数字签名(根据密钥自动检测) |
| **随机数** | SHA3-512(熵) | FIPS 202 | 所有随机生成通过集中式 KDF |
### 信封版本控制
每个加密数据块以 4 字节头部开头,编码所使用的算法:
```
byte 0: 0xE1 (envelope magic)
byte 1: KEM (0x02 ML-KEM-1024 / 0x03 ML-KEM-1024+P-384)
byte 2: Cipher(0x02 XChaCha20-Poly1305)
byte 3: KDF (0x02 SHAKE256)
```
组件可独立替换而无需重新加密现有数据。当 HQC 或未来算法标准化时,分配新 ID,现有数据块仍可读。
API 负载加密(ECIES 密钥交换)拥有独立的协议版本字节 — `_ek` 字段前缀为 `0x01`,标识 ML-KEM-1024 + P-384 + HKDF-SHA3-512 + XChaCha20-Poly1305。未来 KEM 将获得新版本字节;客户端拒绝未知版本。
### 混合 KEM
```
ML-KEM-1024 encapsulate --> shared_secret_1 (32 bytes)
P-384 ephemeral ECDH --> shared_secret_2 (48 bytes)
|
SHAKE256(ss1 || ss2, 32)
|
XChaCha20-Poly1305(key, nonce=24) --> ciphertext
```
同时抵御量子(ML-KEM)与经典(P-384)攻击。若任一被破解,另一仍提供保护。
## 加密架构
零明文存在于任何地方。所有数据在接触磁盘前均被加密或哈希:
```
ML-KEM-1024 + P-384 (vault.key)
|
+-- vault.seal() = hybrid KEM --> SHAKE256 KDF --> XChaCha20-Poly1305
|
+-- Wraps per-file XChaCha20-Poly1305 keys (file encryption at rest)
+-- Wraps per-session XChaCha20-Poly1305 keys (API payload encryption)
+-- Hybrid ECIES key exchange for API clients (ML-KEM-1024 + ECDH P-384 + HKDF-SHA3-512)
+-- Wraps database file XChaCha20-Poly1305 key (DB encryption at rest)
+-- Directly seals ALL database fields (not just PII)
+-- Directly seals session cookie values
```
### 自动字段级加密
路由不会直接调用 `vault.seal()`。集中化的 **字段加密中间件**(`lib/field-crypto.js`)拦截所有数据库操作:
```
Routes pass PLAINTEXT
|
Collection.insert() / update() / find()
|
field-crypto.js (automatic middleware)
|
+-- sealDoc() on write ---> vault.seal() per field ---> DB stores ciphertext
+-- unsealDoc() on read ---> vault.unseal() per field ---> routes get plaintext
+-- derived hashes --------> emailHash, shareIdHash auto-computed
+-- _translateQuery() -----> { email: "x" } becomes { emailHash: sha3("hs-email:x") }
```
每个字段被归类为 `seal`(加密)、`hash`(单向查找)、`derived`(自动从另一字段计算)或 `raw`(ID、时间戳、计数器)。架构在 `FIELD_SCHEMA` 中定义一次,并在每次数据库操作时强制执行。
### 数据加密与密钥保护
| 数据 | 加密 | 密钥保护 |
|------|------|----------|
| **文件内容** | XChaCha20-Poly1305(每个文件独立密钥) | 密钥通过混合 ML-KEM-1024 + P-384 保管库密封 |
| **保管库文件** | ML-KEM-1024 + SHAKE256 + XChaCha20-Poly1305(客户端侧) | 密钥由 passkey 派生(永不离开浏览器) |
| **API 请求/响应体** | XChaCha20-Poly1305(每个会话独立密钥) | 密钥通过混合保管库密封 |
| **磁盘上的数据库文件** | XChaCha20-Poly1305(随机密钥) | 密钥通过混合保管库密封 |
| **会话 Cookie** | 混合 KEM + XChaCha20-Poly1305 | 每个 Cookie 独立调用 `vault.seal()` |
| **所有用户字段**(邮箱、姓名、头像、googleId) | 混合 KEM + XChaCha20-Poly1305 | 自动调用 `vault.seal()` |
| **所有文件元数据**(名称、路径、MIME、存储) | 混合 KEM + XChaCha20-Poly1305 | 自动调用 `vault.seal()` |
| **审计日志字段**(操作、邮箱、详情) | 混合 KEM + XChaCha20-Poly1305 | 自动调用 `vault.seal()` |
| **审计日志 IP** | SHA3-512 哈希后再密封 | 单向哈希,随后自动密封 |
| **密码** | Argon2id | 单向哈希(无需密钥) |
| **邮箱/IP 查询** | SHA3-512 | 用于索引查询的单向哈希 |
### 反攻击保护
| 攻击 | 防护 |
|------|------|
| 量子计算机密钥恢复 | 混合 ML-KEM-1024 + P-384 ECDH(双重保护) |
| 收获现在解密以后 | ML-KEM-1024 后量子 KEM + 信封版本控制以实现算法灵活性 |
| | 经典仅 TLS 降级 | 客户端 Hello PQC 网关拒绝不支持混合密钥交换组的连接 |
| 暴力破解密码 | Argon2id(64MB 内存,3 次迭代) |
| 暴力破解登录 | 速率限制(每 15 分钟 5 次尝试) |
| 暴力破解共享 ID | 256 位 SHA3 派生 ID(2^256 搜索空间) |
| 会话劫持 | 混合 KEM 加密 Cookie,每会话密钥 |
| API 重放攻击 | 时间戳校验(30 秒窗口) |
| API 负载篡改 | XChaCha20-Poly1305 认证(Poly1305 MAC) |
| 数据库文件被盗 | XChaCha20-Poly1305 在静态加密,密钥需保管库.key |
| PII 从数据库转储泄露 | 每个字段密封,IP 单向哈希 |
| 非重复数碰撞 | XChaCha20 192 位随机数消除生日界限风险 |
| AES-NI 侧信道 | XChaCha20 软件实现为常数时间,无硬件依赖 |
| 捆绑包密码暴力破解 | 5 次失败后指数退避锁定(2^n × 30 秒) |
| 邮箱枚举在捆绑包上 | 无论邮箱是否在允许列表中均返回相同响应 |
| 捆绑包访问码暴力破解 | 每个码 5 次尝试限制,速率限制,10 分钟过期 |
| CSRF(API 端点) | 每个会话使用 XChaCha20-Poly1305 密钥绑定 JSON 请求;表单 POST 使用常数时间 CSRF 令牌验证 |
| 登出 CSRF | 登出仅允许 POST 并验证 CSRF 令牌 — 跨站 `
![]()
` 或 `
` 无法强制登出 |
| WebSocket 凭证泄露 | API 密钥仅通过 Authorization 头接受 禁止查询字符串令牌以防止代理/日志/Referer 泄露 |
| 会话密钥拦截 | 混合 ECIES 密钥交换 — 会话密钥通过 ML-KEM-1024 + ECDH P-384 加密,永不明文传输 |
| CSV 公式注入 | 导出值清洗以防止电子表格代码执行 |
| DNS 重绑定通过 Webhook | 出站连接预验证 IP 固定 |
| SSRF 通过 Webhook | 阻止 localhost、RFC 1918、RFC 6598 CGNAT、链路本地、IPv6 私有范围 |
| 伪装文件上传 | 魔术字节验证拒绝内容与扩展名不匹配的文件 |
| 恶意文件名 | 后端清洗控制字符、路径遍历、点攻击、HTML 注入 |
| ZIP 路径遍历(Zip Slip) | 条目名称清洗移除 `..` 段,上传与归档时路径标准化 |
| 匿名存储滥用 | 基于 IP 的上传配额,24 小时滚动窗口 |
| 存储型 XSS 通过上传 | 用户可控名称在模板中自动转义;原始输出仅限管理员设定值 |
| 弱捆绑/保管库密码 | 服务器端强制执行最小 4 字符要求 |
| 自动化扫描器与机器人 | 请求指纹识别(accept-language、sec-fetch-dest、sec-fetch-mode)在公共路由阻止非浏览器客户端 — 即便采用 PQC TLS 也有效 |
| NPM 供应链 | 所有依赖项寄存在仓库中 — 运行时零 npm 包 |
| 管理员设置注入 | 类型安全设置架构(lib/settings-schema.js)清洗保存(清洗控制字符、去除空格、类型特定规范化)并校验(格式、范围、枚举) — 错误数据在入口处被拒绝并给出明确错误 |
| 过期陈旧配置 | 配置重置注册表(config.onReset)在依赖设置变更时使缓存客户端失效(S3 等) |
基于 Node.js 24.8+(LTS),通过 OpenSSL 3.5 使用 ML-KEM-1024、ML-DSA-87 与 SLH-DSA-SHAKE-256f,XChaCha20-Poly1305 与 SHAKE256 通过寄存的 @noble/ciphers 与 @noble/hashes,Argon2id 通过原生预构建包,WebAuthn 通过寄存的 @simplewebauthn/server,内置 SQLite(`node:sqlite`)。零 npm 运行时依赖。
## 功能特性
**认证**
- Argon2id 本地认证、Google OAuth、WebAuthn 通行密钥 — 可同时使用
- TOTP 双因素认证,含一次性备用码
- 使用 SHA3 哈希的邮箱验证令牌
- 混合 KEM 加密会话 Cookie
- 每会话 XChaCha20-Poly1305 加密 API 负载,含反重放与反篡改
- 混合 ECIES 密钥交换用于 API 客户端 — ML-KEM-1024 + ECDH P-384 + HKDF-SHA3-512 + XChaCha20-Poly1305(响应中无明文密钥)
- 登录(5/15 分钟)、注册(10/15 分钟)、2FA 验证(5/5 分钟)、通行密钥登录(10/分钟)速率限制
- 连续 10 次密码失败后账户锁定(30 分钟冷却)
- 密码重置流程含一次性 1 小时过期令牌及反枚举(始终返回成功)
- 用户邀请系统 — 管理员通过邮箱邀请并分配角色,48 小时过期
- 可配置会话空闲超时(默认 30 分钟,服务端强制执行)
- Google 回调的 OAuth CSRF 状态校验
- 修改密码自动吊销其他会话
**文件管理**
- 公共文件夹投放 — 拖拽整个目录树,无需登录
- 每文件 XChaCha20-Poly1305 加密,密钥通过混合 ML-KEM-1024 + P-384 密封
- 大文件分块上传(>10MB 自动拆分,服务端重组)
- 上传暂停/恢复/取消,每文件进度条
- 密码保护分享链接,指数退避锁定(2^n × 30 秒,5 次失败后)
- 邮箱访问限制 — 限制捆绑包至特定收件邮箱,通过一次性验证码验证(反枚举、速率限制、SHA3 哈希码)
- 双重保护模式 — 要求邮箱验证与密码双重保护
- 可配置每捆绑包过期时间(1d、7d、30d、90d、永不)
- 捆绑包消息、多收件人邮箱
- 捆绑包命名 — 上传时命名,仪表板内重命名
- 文件与捆绑包内重命名,后端强制执行清洗(防点攻击、路径遍历、扩展名保留)
- 魔术字节内容验证 — 上传文件按声明的扩展名验证(15 种格式签名)
- 文件预览与 SVG 清洗,强制下载 HTML/JS
- 可分享链接 — 浏览文件夹或下载为 ZIP
- 子文件夹 ZIP 下载 — 从捆绑包下载单个子目录
- 安全的 Content-Disposition 头部(RFC 5987 编码非 ASCII 文件名)
**零知识保管库**
- 浏览器端 ML-KEM-1024 + SHAKE256 KDF + XChaCha20-Poly1305 加密
- 通行密钥门禁(Touch ID、Face ID、YubiKey、FIDO2)
- PRF 模式实现真零知识(密钥永不触达服务器)
- 隐身模式隐藏保管库操作于审计日志
- 直接访问链接用于通行密钥认证的保管库文件下载
- 保管库密钥轮换,原子化重加密所有文件
- 客户端生成批量上传与批量删除(含客户端生成的批量 ID)
- 保留文件夹结构的保管库上传与批量 ZIP 下载
- 捆绑包与单个文件的内联重命名
- 强制重置恢复模式用于保管库锁定(删除所有保管库文件并清除状态)
- 仅 ML-KEM-1024(ML-KEM-768 完全移除 — 服务器启动时拒绝 768 密钥)
**客户存储 — 品牌化上传门户**
- 在 `/stash/:slug` 创建自定义品牌化上传页面
- 每页品牌 — 自定义标题、说明、强调色、Logo
- 每页上传约束 — 最大文件大小、最大文件数、默认过期、允许扩展名
- 使用 Argon2 哈希的密码保护页面,速率限制解锁
- 邮箱/域名访问限制 — 限制至特定邮箱或整个域名(@acme.com),通过一次性验证码验证
- 双重保护模式 — 在保管库页面上要求邮箱验证与密码
- 简化上传表单 — 仅消息与文件(无姓名/邮箱字段)
- 上传时命名捆绑包
- 动态 slug 校验,自动检测保留路由
- 记录每页存储统计(捆绑包数量、总字节数)
- 每页自定义 Logo 上传与魔术字节验证
- 专用管理页面 — 捆绑包明细浏览、文件浏览、内联重命名、删除、全部清除
- 管理员管理 — 创建、编辑、切换、复制链接、删除保管库页
**团队**
- 创建团队,添加/移除成员并分配角色
- 团队范围文件可见性 — 跨团队隔离强制执行
- 团队管理员与成员角色
**个人资料**
- 支持自我修改邮箱(需重新认证密码)
- 自我删除账户(文件重新分配、会话吊销、最后管理员受保护)
**管理面板**
- 统计(含计算总量:大小、下载量)、活动日志
- 基于行的捆绑包列表与文件明细(我的存储 + 个人保管库)
- 分页文件/捆绑包浏览器与搜索
- 用户管理 — 创建、挂起、删除、角色切换
- 审计日志 — 可搜索、可过滤、日期范围
- 设置面板 — 9 个标签(品牌、通用、认证、上传、存储、主题、邮件、环境、备份)
- API 密钥(作用域权限:upload、read、admin、webhook)
- Webhook(HMAC-SHA3-512 签名负载,每钩子投递日志,启用/禁用开关)
- IP 阻断列表
- 数据库备份(提供加密副本),CSV 导出(含公式注入防护)
- 自动化异地备份至 S3 兼容存储(AWS、R2、MinIO、B2、 Spaces),含通行短语加密的保管库密钥、增量文件清单、可配置保留策略及管理 UI 手动触发
- 定时任务 — 文件过期、审计保留、滞留下传清理、令牌清理、邀请清理、每日 SQLite 真空、自动备份
- 危险区 — 工厂重置、清除所有会话、清除所有用户、清除所有文件(需确认)
- 自定义 Logo 上传与魔术字节验证、SVG 清洗
- 反向代理自动检测与配置片段生成(nginx、Caddy、Apache)
- 每用户存储配额(独立于全局配额)及每 IP 公共上传配额(24 小时滚动窗口)
- 可配置上传并发数、重试次数、超时、文件扩展名允许列表
- 管理员邮箱列表用于自动提升 OAuth 用户至管理员角色
- 维护模式 — 阻止非管理员访问并返回 503 页面
- 公告横幅 — 全站页面显示的文本
**邮件**
- SMTP 或 Resend API 后端(管理端可切换)
- 双模式故障转移 — SMTP 主 / Resend 备 或 Resend 主 / SMTP 备
- Resend 配额执行(每日/每月限制,按计划层级)
- 邮件模板自定义 — 主题、页眉、页脚(支持命名占位符:{siteName}、{uploaderName}、{fileCount}、{totalSize})
- 上传确认、管理员通知、验证邮件
- 所有邮件发送/失败/配额事件均审计记录
**同步与 API**
- 可变同步捆绑包 — `bundleType: "sync"` 创建持久、可变的捆绑包,支持创建后添加/替换/删除文件
- 文件替换 — 上传同名 `relativePath` 覆盖文件(旧密钥与数据块完全移除)
- 文件重命名/移动 — `POST /sync/rename` 更新 `relativePath`(元数据操作,不重新上传),触发 `file_renamed` WebSocket 事件。同步客户端通过校验和匹配在去抖窗口内检测本地重命名
- 文件删除 — 单个文件可通过软删除(30 天清理)从同步捆绑包移除
- 每文件变更跟踪 — `seq` 单调计数器和 `updatedAt` 时间戳用于同步变更源
- JSON 内容协商 — 捆绑包查看时设置 `Accept: application/json` 返回含校验和与元数据的文件列表
- 为文件变更结构化审计日志事件(含操作、捆绑包 ID、校验和、大小)
- 共享访问控制中间件(`require-access.js`)— 捆绑包与存储桶的集中锁检查
- JSON 感知认证 — API / 同步客户端收到 401 JSON,浏览器收到登录重定向
- WebSocket 同步通道 — `GET /sync/ws` 升级握手时认证,作用域限定至单个捆绑包
- 实时文件变更事件(file_added、file_replaced、file_removed、心跳)
- 重连时通过 seq 游标恢复(`?since=N`)
- PQC TLS 强制 — ClientHello 检查拒绝无混合密钥交换组的连接
- PQC 网关架构 — TCP 代理检查 `supported_groups` 扩展后再完成 TLS 握手
- 本地主机旁路 Docker 健康探测(127.0.0.1/::1 跳过 PQC 检查)
- `PQC_ENFORCE=false` 临时禁用网关(仅推荐过渡期)
- PQC TLS — 条件 HTTPS,使用 SecP384r1MLKEM1024 > X25519MLKEM768 > SecP256r1MLKEM768 混合密钥交换(仅 TLS 1.3,优先 Level 5)
- Let's Encrypt 证书自动重载(每小时文件轮询)
- PQC 出站 HTTPS 代理 — 所有 S3、SMTP、Resend、Webhook、OAuth 调用使用 PQC 混合 TLS 组
- `PQC_OUTBOUND_ENFORCE=false` 允许出站连接的降级回经典
- mTLS 用于同步客户端 — 服务器充当自己的 CA(ECDSA P-384)
- 同步令牌创建时生成客户端证书并提供一键 PEM 捆绑下载
- 证书吊销表(SHA3-512 哈希指纹查找)
- WebSocket 升级验证 mTLS 证书 + API 密钥(双重认证,任一单独不足)
- `MTLS_REQUIRED=true` 强制所有同步连接使用客户端证书
- 新增 `sync` API 密钥作用域(WebSocket 连接与同步捆绑包操作)
- 资源作用域 API 密钥 — `boundStashId` 与 `boundBundleId` 列限制密钥至特定资源
- 存根作用域令牌 — 管理员生成仅授予单一存根访问权限的令牌
- 一次性注册码 — 管理员生成短码(如 `HSTASH-A4K9-XMWP-7RB2`),客户端兑换自动获取 API 密钥与 mTLS 证书(无需文件传输,1 小时过期)
- 存根同步模式 — 每个存根对应持久可变捆绑包,用于桌面同步客户端
- 管理 UI:每存根切换同步、一次性生成同步令牌并复制
- 桌面同步客户端:[hermitstash-sync](https://github.com/dotCooCoo/hermitstash-sync) — 监视本地文件夹并通过 WebSocket + PQC TLS 同步
**安全加固**
- 所有响应添加安全头(CSP、X-Frame-Options、nosniff、Referrer-Policy、Permissions-Policy、COOP、CORP)
- HSTS 在 rpOrigin 使用 HTTPS 时自动启用预加载
- 内容安全策略禁止外部域名(字体本地托管、`object-src 'none'`、`base-uri 'none'`、`frame-ancestors 'none'`)
- 256 位 SHA3 派生共享 ID(无暴力、无碰撞)
- CSRF 保护:JSON 请求绑定会话加密密钥;表单 POST 使用常数时间 CSRF 令牌;非 JSON/非豁免 POST 拒绝
- 登出仅允许 POST 并验证 CSRF 令牌(无 GET 登出 CSRF)
- 机器人防护中间件 — 请求指纹识别(accept-language、sec-fetch-dest、sec-fetch-mode)阻止自动化扫描器在公共路由运行,不依赖 User-Agent
- WebSocket API 密钥仅通过 Authorization 头接受 — 禁止查询字符串令牌以防代理/日志/Referer 泄露
- CSV 公式注入防护在所有导出中启用
- 可配置 CORS(通配符禁止携带凭据)
- 健康端点 CORS 可从管理端配置,供 PQC 网关状态检查
- 规范源策略 — 所有 URL 均从 rpOrigin 生成,绝不使用 Host 头
- Webhook DNS 固定 — 解析的 IP 在出站连接中复用,防止 TOCTOU 重绑定
- 所有自由文本字段输入长度限制
- 分页上限 200 条结果
- X-Forwarded-For 仅信任配置代理
- 安全重定向(仅相对路径)
- SSRF 防护覆盖 RFC 1918、RFC 6598 CGNAT、链路本地、元数据及 IPv6 私有范围
- 所有加密与字体依赖项均寄存 — 零外部 CDN 请求,零运行时包
- 用户上传 Logo 目录的限制性 CSP(纵深防御防 SVG XSS)
**存储**
- 本地磁盘、NAS 挂载或任意 S3 兼容存储(MinIO、Cloudflare R2、DigitalOcean Spaces、Backblaze B2)
- S3 直接下载带预签名 URL(可配置过期时间,AWS Signature V4)
- 每文件 XChaCha20-Poly1305 在静态加密,密钥通过混合保管库密封
**SEO 与法律**
- 所有页面的 Open Graph 与 Twitter Card 元标签,动态使用站点名与源
- 规范 URL 标签源自 rpOrigin
- robots.txt 屏蔽 admin、dashboard、vault、auth 页面
- 动态 sitemap.xml(`GET /sitemap.xml`)包含公开页面
- 所有认证页面添加 noindex/nofollow 元标签
- 可配置隐私政策、服务条款与 Cookie 策略页面
- 默认法律页面模板(含自托管部署的合理内容)
- 页脚链接至所有法律页面
- 可注入分析脚本(Plausible、Umami、Matomo、Fathom、PostHog、Google Analytics)
- 分析仅注入公共页面(管理/仪表板排除)
- API 加密作用域同源 — 外部分析与第三方请求原样透传
- 从分析脚本自动检测 CSP 域并可手动覆盖
**可访问性**
- 键盘导航的“跳至内容”链接
-交互控件(主题切换、图标按钮)的 ARIA 标签
- 所有 Logo 与头像图片的 alt 文本
- 使用 `` 地标的语义 HTML
**零配置**
- 无 `.env` 文件 — 设置存储在加密数据库
- 无构建步骤 — 原生 Node.js
- `node server.js` 即为全部设置 — 无需 npm 安装
- `process.env` 覆盖可用于 Docker/容器
- 健康检查端点(`GET /health`)用于负载均衡器、容器探测与 PQC 网关状态检查(可配置 CORS)
- 零外部 CDN 依赖 — 字体本地寄存,无请求到 Google、Cloudflare 或任何第三方
- PWA Web 应用清单,动态站点名与主题色
- 启动时自动数据库架构迁移
- 启动不变性检查 — 校验保管库密钥、警告默认凭据/密钥、检查目录权限
## Docker 部署
### 快速启动(预构建镜像)
```
docker pull ghcr.io/dotcoocoo/hermitstash:latest
docker run -d --name hermitstash \
-p 3000:3000 \
-v ./data:/app/data \
-v ./uploads:/app/uploads \
--shm-size=256m \
ghcr.io/dotcoocoo/hermitstash:latest
```
或使用 docker compose(预构建镜像):
```
services:
hermitstash:
image: ghcr.io/dotcoocoo/hermitstash:latest
ports: ["3000:3000"]
volumes:
- ./data:/app/data
- ./uploads:/app/uploads
shm_size: 256m
environment:
TRUST_PROXY: "true"
RP_ORIGIN: "" # https://your-domain.com
restart: unless-stopped
```
### 从源码快速构建
```
git clone https://github.com/dotCooCoo/hermitstash.git
cd hermitstash
docker compose up -d
```
使用 `node:24-slim`(OpenSSL 3.5+ 支持 PQC)。无需配置文件 — 所有依赖项寄存,无需 `npm install`。启动时使用默认设置并在首次运行生成保管库密钥对。配置全部通过 `/admin` 面板完成。
### 镜像详情
| | |
|----|---|
| **基础镜像** | `node:24-slim`(Debian Bookworm) |
| **Node.js** | 24.8+(运行 ML-KEM-1024、ML-DSA-87、SLH-DSA-SHAKE-256f 所需,OpenSSL 3.5) |
| **用户** | 以 `hermit`(非 root)身份运行,通过 `gosu` 切换 — 入口点修复卷权限后降权 |
| **临时文件系统** | `HERMITSTASH_TMPDIR=/dev/shm` — 明文数据库驻留内存,永不落盘。Compose 中设置 `shm_size: 256m` |
| **卷** | `/app/data`(加密数据库、密钥、TLS 证书)、`/app/uploads`(本地存储文件) |
| **端口** | 3000(可通过 `PORT` 环境变量配置) |
| **健康检查** | 内置:`GET /health` 每 30 秒,超时 5 秒,重试 3 次,启动期 10 秒 |
| **入口点** | `docker-entrypoint.sh` — 变更卷属主为 `hermit:hermit`,然后执行 `gosu hermit node server.js` |
### docker-compose.yml
内置的 `docker-compose.yml` 提供生产就绪起点:
```
services:
hermitstash:
build: .
ports:
- "3000:3000"
volumes:
- ./data:/app/data # encrypted DB, vault keys, TLS certs
- ./uploads:/app/uploads # files (local storage only)
shm_size: 256m # /dev/shm for plaintext DB in memory
environment:
NODE_ENV: production
HERMITSTASH_TMPDIR: /dev/shm
PORT: 3000
TRUST_PROXY: "true" # set if behind nginx/Cloudflare/Coolify
RP_ORIGIN: "" # https://your-domain.com (required for passkeys + HSTS)
restart: unless-stopped
```
其他所有设置(认证、邮件、S3、品牌)均建议通过 `/admin` 面板配置,以便凭据以加密形式存入数据库。环境变量可覆盖数据库设置,并在管理 > 设置 > 环境 标签页可见。
### Coolify / 托管 Docker 主机
开箱即用于 Coolify、Portainer、CapRover 等平台:
1. 将平台指向代码仓库(或 Dockerfile)
2. 设置 `RP_ORIGIN` 环境变量为完整域名(如 `https://app.hermitstash.com`)
3. 挂载持久卷 `/app/data` 与 `/app/uploads`
4. 设置共享内存为 256MB(或等效容器配置)
5. 内置健康检查与任何支持 `HEALTHCHECK` 的编排器兼容
### TLS / HTTPS
服务器可自行终止 TLS(用于 PQC 强制)或置于反向代理后:
- **Cloudflare/nginx(推荐)**:设置 `TRUST_PROXY=true`。代理终止 TLS;服务器内部运行 HTTP。PQC 终端在 Cloudflare 边缘处理。设置 `PQC_ENFORCE=false` 若代理到服务器链路为明文 HTTP。
- **直接 TLS(PQC 强制)**:挂载 TLS 证书至 `/app/data/tls/fullchain.pem` 与 `/app/data/tls/privkey.pem`(或通过 `TLS_CERT` 与 `TLS_KEY` 环境变量)。PQC 网关检查 ClientHello 并拒绝非 PQC 连接组。服务器协商 `SecP384r1MLKEM1024 > X25519MLKEM768 > SecP256r1MLKEM768`(最强可用混合组)。Let's Encrypt 证书自动重载(每小时文件轮询)。
### 持久化数据
| 路径 | 内容 | 是否备份 |
|------|------|----------|
| `/app/data/hermitstash.db.enc` | 加密的 SQLite 数据库(用户、文件、设置、审计日志) | 是 — 提供自动化 S3 备份 |
| `/app/data/vault.key` | ML-KEM-1024 + P-384 混合密钥对(加密所有数据库字段) | **关键** — 丢失此文件则所有加密数据无法恢复 |
| `/app/data/tls/` | TLS 证书(如使用直接 TLS) | 由 Let's Encrypt 重新生成 |
| `/app/uploads/` | 上传文件(仅限本地存储;非必需) | 可选 — 文件可重新上传 |
### 健康检查
`GET /health` 返回 `{ status, uptime, timestamp }` — 适用于 Docker HEALTHCHECK、Kubernetes 存活探针、负载均衡器以及 [PQC 网关](https://github.com/dotCooCoo/hermitstash-web) 状态检查(可配置 CORS)。
### 反向代理
如需前置 nginx、Caddy 或 Apache:在管理面板(设置 > 上传)会自动检测代理并生成可粘贴的配置片段,包含正确的主体大小限制。
### S3 存储
在管理 > 设置 > 存储 标签页配置 S3 兼容存储(AWS、MinIO、Cloudflare R2、DigitalOcean Spaces、Backblaze B2)。所有凭据均加密存储并在保存时通过设置架构校验。对于 R2,将端点设为 `https://.r2.cloudflarestorage.com`,区域设为 `auto`。
### 其他平台
**Coolify / Portainer**:使用镜像 `ghcr.io/dotcoocoo/hermitstash:latest`。设置 3000 端口,挂载 `/app/data` 与 `/app/uploads` 为持久卷,设置共享内存 256MB,添加 `TRUST_PROXY=true` 与 `RP_ORIGIN=https://your-domain.com`。
**Unraid**:Docker → 添加容器 → 粘贴以下模板 URL:
```
https://raw.githubusercontent.com/dotCooCoo/hermitstash/main/unraid-template.xml
```
自动填充图标、端口、卷并添加 `--shm-size=256m`。
**Synology / QNAP**:容器管理器 → 注册表 → 添加 `ghcr.io`,搜索 `dotcoocoo/hermitstash`,下载 `latest`。创建容器并映射 3000 端口及 `/app/data`、`/app/uploads` 文件夹。对于 `--shm-size`,通过 SSH 执行:`docker run -d --shm-size=256m ...`
**Kubernetes**:
```
curl -O https://raw.githubusercontent.com/dotCooCoo/hermitstash/main/kubernetes.yml
# 编辑 RP_ORIGIN、PVC 大小和可选的 Ingress
kubectl apply -f kubernetes.yml
kubectl port-forward -n hermitstash svc/hermitstash 3000:3000
```
包含:命名空间、PVC、Deployment(含存活/就绪探针、资源限制、内存回 `/dev/shm`)、Service 及注释的 Ingress 模板。
**TrueNAS SCALE**:应用 → 自定义应用 → 镜像 `ghcr.io/dotcoocoo/hermitstash`,标签 `latest`。添加主机路径数据集用于 `/app/data` 与 `/app/uploads`。添加共享内存 emptyDir 卷:类型 Memory,大小 256Mi,挂载于 `/dev/shm`。
### 升级
```
# 备份 vault 密钥(关键 — 丢失 = 所有数据不可恢复)
cp data/vault.key data/vault.key.bak
# 拉取新镜像并重启
docker pull ghcr.io/dotcoocoo/hermitstash:latest
docker compose up -d
```
数据库迁移在启动时自动运行,无需手动操作。服务器日志会在启动时输出已应用的迁移。如遇问题,可还原 `vault.key` 与 `hermitstash.db.enc` 并回退至上一镜像版本。
### 维护模式
在 管理 > 设置 > 品牌 页面切换。阻止所有非管理员访问并返回 503 页面。管理员路由、认证路由及作用域 API 密钥仍可在维护期间正常工作。
## API 密钥
API 密钥支持程序化访问。在管理面板的 **API 密钥** 可折叠区域管理。
### 创建密钥
可通过管理面板或 API 生成密钥:
```
curl -X POST https://your-domain/admin/apikeys/create \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline", "permissions": "upload"}'
```
响应(密钥仅显示一次,随后存储 SHA3 哈希 — 不可恢复):
```
{ "success": true, "key": "hs_a1b2c3d4e5f6...", "prefix": "hs_a1b2" }
```
### 认证
通过 Bearer 令牌使用:
```
Authorization: Bearer hs_a1b2c3d4e5f6...
```
### 权限作用域
| 作用域 | 权限 |
|--------|------|
| `upload` | 创建捆绑包、通过 `/drop` 上传文件 |
| `read` | 列出与下载文件、查看捆绑包 |
| `admin` | 完整管理权限(设置、用户、Webhook、密钥) |
| `webhook` | 管理 Webhook |
### 上传端点
公共上传端点接受 API 密钥认证。认证后上传归属于密钥所有者。
| 端点 | 方法 | 描述 |
|------|------|------|
| `POST /drop/init` | 初始化捆绑包。返回 `bundleId`、`shareId`、`finalizeToken` |
| `POST /drop/file/:bundleId` | 上传文件(multipart/form-data,字段 `file`) |
| `POST /drop/chunk/:bundleId` | 分块上传大文件(multipart,字段:`chunk`、`filename`、`chunkIndex`、`totalChunks`) |
| `POST /drop/finalize/:bundleId` | 完成捆绑包。请求体:`{ "finalizeToken": "..." }` |
### 示例:程序化上传
```
# 1. Init bundle
INIT=$(curl -s -X POST https://your-domain/drop/init \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"password": "", "message": "Automated upload", "expiryDays": 7}')
BUNDLE_ID=$(echo $INIT | jq -r '.bundleId')
TOKEN=$(echo $INIT | jq -r '.finalizeToken')
SHARE_ID=$(echo $INIT | jq -r '.shareId')
# 2. Upload file
curl -X POST "https://your-domain/drop/file/$BUNDLE_ID" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@report.pdf"
# 3. Finalize
curl -X POST "https://your-domain/drop/finalize/$BUNDLE_ID" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"finalizeToken\": \"$TOKEN\"}"
echo "Share link: https://your-domain/b/$SHARE_ID"
```
### 管理端点
均需 `admin` 作用域:
| 端点 | 方法 | 描述 |
|------|------|------|
| `GET /admin/apikeys/api` | 列出所有 API 密钥(隐藏哈希) |
| `POST /admin/apikeys/create` | 生成新密钥。请求体:`{ "name": "...", "permissions": "upload" }` |
| `POST /admin/apikeys/:id/revoke` | 永久吊销密钥 |
| `GET /admin/settings` | 获取全部设置(敏感值已屏蔽) |
| `POST /admin/settings` | 更新设置。请求体:`{ "siteName": "...", ... }` |
| `GET /admin/environment` | 运行时信息(Node.js、OpenSSL、Docker、环境变量覆盖) |
| `GET /admin/apikeys/api` | 列出所有 API 密钥(隐藏哈希) |
| `POST /admin/apikeys/create` | 生成新密钥。请求体:`{ "name": "...", "permissions": "upload" }` |
| `POST /admin/apikeys/:id/revoke` | 永久吊销密钥 |
| `GET /admin/settings` | 获取全部设置(敏感值已屏蔽) |
| `POST /admin/settings` | 更新设置。请求体:`{ "siteName": "...", ... }` |
| `GET /admin/environment` | 运行时信息(Node.js、OpenSSL、Docker、环境变量覆盖) |
## Webhook
Webhook 在事件发生时发送带签名的 HTTP POST 请求。在管理面板的 **Webhook** 可折叠区域管理。
### 创建 Webhook
```
curl -X POST https://your-domain/admin/webhooks/create \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hook", "events": "*"}'
```
响应(密钥仅显示一次):
```
{ "success": true, "secret": "a1b2c3d4..." }
```
### 事件
| 事件 | 触发条件 | 负载 |
|------|----------|------|
| `bundle_finalized` | 捆绑包上传完成并最终化 | `{ shareId, uploaderName, files, size }` |
事件过滤器:设为 `*` 接收全部事件,或指定具体事件名称。将来版本可能新增事件。
### 负载格式
```
{
"event": "bundle_finalized",
"data": {
"shareId": "a1b2c3d4e5f6...",
"uploaderName": "Anonymous",
"files": 3,
"size": 1048576
},
"timestamp": "2026-04-09T12:00:00.000Z"
}
```
### 签名验证
每个 Webhook 请求包含 `X-Webhook-Signature` 头,其值为对原始 JSON 体的 HMAC-SHA3-512 十六进制摘要,使用 Webhook 密钥签名:
```
X-Webhook-Signature: a1b2c3d4e5f6...
```
在处理器中验证:
```
const crypto = require("crypto");
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac("sha3-512", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
```
```
import hmac, hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha3_512).hexdigest()
return hmac.compare_digest(signature, expected)
```
### SSRF 防护
Webhook URL 校验规则:
- 私有 IP 段(127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)
- 链路本地地址(169.254.0.0/16、fe80::/10)
- IPv6 私有范围(fc00::/7、::1)
- 云元数据端点(169.254.169.254)
- 拒绝生产环境中的非 HTTPS 方案
## 关键文件
全部位于 `data/` 目录(已被 .gitignore 忽略):
| 文件 | 说明 | 是否丢失 |
|------|------|----------|
| `data/vault.key` | ML-KEM-1024 + P-384 混合密钥对 | **所有加密数据永久不可恢复** |
| `data/db.key.enc` | 数据库文件加密密钥(保管库密封) | 数据库文件无法读取 |
| `data/hermitstash.db.enc` | 加密的 SQLite 数据库 | 所有设置、用户、审计日志丢失 |
**请备份 `data/vault.key`。** 这是整个加密链的根。此密钥无法再生,每一个密封值、加密文件、受保护密钥均由此派生。
## 寄存依赖
所有运行时依赖均提交至仓库 —— 无需 `npm install`。通过 `scripts/vendor-update.sh` 管理:
```
./scripts/vendor-update.sh --check # see what's outdated
./scripts/vendor-update.sh --diff @noble/ciphers # see changelog
./scripts/vendor-update.sh @noble/ciphers 2.2.0 # update a package
```
| 包 | 版本 | 作者 | 用途 |
|----|------|------|------|
| [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers) | 2.1.1 | [Paul Miller](https://github.com/paulmillr) | XChaCha20-Poly1305(服务端 + 浏览器) |
| [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | 2.0.1 | [Paul Miller](https://github.com/paulmillr) | SHAKE256 KDF(浏览器) |
| [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) | 0.6.0 | [Paul Miller](https://github.com/paulmillr) | ML-KEM-1024(浏览器保管库 + 服务端 ECIES) |
| [`@simplewebauthn/server`](https://github.com/MasterKale/SimpleWebAuthn) | 13.3.0 | [Matthew Miller](https://github.com/MasterKale) | WebAuthn/通行密钥验证 |
| [`argon2`](https://github.com/ranisalt/node-argon2) | 0.44.0 | [Ranieri Althoff](https://github.com/ranisalt) | 密码哈希(原生预构建包,8 个平台) |
这些库工作出色。感谢 Paul Miller 的杰出贡献。全部采用 MIT 许可。
## 架构
100+ 个 JS 文件、26 个 HTML 模板、20 张数据库表。小文件、单任务。
```
server.js Bootstrap, middleware, scheduled tasks, default accounts
lib/
crypto.js PQC crypto: ML-KEM-1024+P-384, XChaCha20, SHAKE256,
ML-DSA-87, SLH-DSA-SHAKE-256f, envelope versioning
vault.js Hybrid keypair management, seal/unseal, auto key upgrade
field-crypto.js FIELD_SCHEMA: auto seal/unseal/hash for all DB fields
db.js SQLite + auto field crypto + DB file encryption
api-crypto.js API payload XChaCha20-Poly1305 encrypt/decrypt
session.js Hybrid KEM encrypted cookies, LRU eviction
storage.js Local/S3 + XChaCha20-Poly1305 file encryption + pre-signed URLs
config.js Settings from encrypted DB, env fallback, onReset registry
settings-schema.js Type-safe settings sanitization + validation (74 settings)
audit.js Audit logging with auto-sealed entries
rate-limit.js Per-IP rate limiting with proxy validation
ip-quota.js Per-IP storage quota for anonymous uploads
email.js SMTP + Resend API with dual failover + quota tracking
router.js HTTP server, routing, pre-compiled patterns
multipart.js Multipart + JSON body parser (shared accumulator)
template.js Custom template engine with caching
sanitize.js Filename sanitization + HTML escaping
sanitize-svg.js SVG sanitizer (strips scripts, events, dangerous tags)
totp.js TOTP generation/verification, backup codes
google-auth.js Google OAuth2 (OpenID Connect, CSRF state)
constants.js Paths, versions, theme, hash prefixes, time constants
zip.js ZIP writer with Deflate compression
expiry.js File expiry cleanup
scheduler.js Task scheduler
webhook.js Webhook dispatch queue
pqc-gate.js ClientHello PQC group inspection at TCP level
pqc-agent.js PQC-only outbound HTTPS agent
vendor/ Vendored dependencies (argon2, noble-*, simplewebauthn)
app/
bootstrap/ Startup invariant checks
data/ Repositories + migration runner
domain/ Services (auth, uploads, teams, admin, webhooks, email)
http/ Request validators (upload magic bytes, auth, admin)
security/ CSRF, CORS, SSRF, scope, origin policies
domain/uploads/ Shared upload handler, bundle service, chunk service
jobs/ Background jobs (expiry, audit retention, webhook dispatch)
shared/ Errors, logger, validation helpers, filename sanitization
scripts/ vendor-update.sh, vendor-font.js, sync-to-public.sh
routes/ 18 route files (includes stash.js for Customer Stash)
middleware/ 12 files (auth, CORS, CSRF, API encryption, security headers, bot guard)
views/ 25 templates
public/ CSS, JS, logos, icons, vendored fonts
```
## 贡献
我坦诚地说:**目前不接受代码贡献**,并说明原因而非简单拒绝。
HermitStash 是一个以安全为核心的项目,由个人维护。评审外部代码贡献到加密系统是我目前无法负责任地完成的工作 —— 我仍在学习,若合并我不懂的代码(不好)或要求贡献者无限期等待(也不好),都会带来问题。诚实的回答是我尚未准备好。
即便如此,仍有许多方式可以帮忙:
- **Bug 报告。** 若不符合预期或行为令人惊讶,请打开问题。提供复现步骤帮助很大。
- **安全发现。** 若发现密码学问题、原始类型误用或与安全声明相悖之处,请私下报告 —— 请参见 [SECURITY.md](SECURITY.md)。
- **功能请求。** 描述使用场景。我不一定实现,但想听到需求。
- **文档反馈。** 若 README 不清晰、错误或缺失,问题是很好的反馈。文档问题是我最有用的反馈之一。
- **提问。** 若使用 HermitStash 遇到不清晰之处,欢迎提问。
若你基于 HermStash 构建了项目,或运行在有趣的环境中,也请打开问题让我知道 —— 仅说一句“你好”也很欢迎。
这可能会改变。若将来 HermitStash 成长到可负责任地评审外部代码,我会更新本节。届时感谢理解,并感谢你一开始就感兴趣。
## 许可证
[AGPL-3.0-or-later](LICENSE)
## 最后的话
若你已读至此 —— 谢谢你。构建并分享 HermitStash 是我做过的最有意义的事之一,而你的关注对我意义重大。
若 HermitStash 对你有帮助并愿意请我喝一杯,可通过 [ko-fi.com/dotcoocoo](https://ko-fi.com/dotcoocoo) 支持我。永远不强制,永远感激。标签:admin email, Docker, FIPS 203, GNU通用公共许可证, HermitStash, MITM代理, ML-KEM-1024, NIST, Node.js, no npm install, P-384, passkey, rpId, rpOrigin, Rust, setup wizard, vendored dependencies, 一次性部署, 可视化界面, 后量子加密, 安全防御评估, 密钥对生成, 开源文件存储, 数据主权, 文件共享, 无依赖, 服务端加密, 混合加密, 漏洞探索, 私有云, 端到端加密, 管理员面板, 网络流量审计, 自举, 自定义脚本, 自托管, 蓝队防御, 请求拦截, 量子安全, 零明文, 零知识存储