免费隐私工具
封存一个文件
用一把全新的一次性密钥加密文件,然后通过你喜欢的任何渠道发出去。拿到密钥的人就在这里、就在本页上把它打开。绝无上传,没有可破解的密码短语,而且在任何东西被保存之前,你就能看清里面是什么。
完全在此标签页中运行。本页根本无法与任何服务器通信。
正在封存…
已封存。
这就是你的加密文件。没有密钥,没有人能读到它。我们不能,任何人都不能。
密钥——只在此刻显示
这把密钥只存在于本页、只存在于此刻。它不会被存储,也不会被发送到任何地方。一旦丢失,没有人能打开这个文件,包括我们。离开之前先把它保存好。
或者把密钥作为链接分享
密钥就跟在链接中 # 的后面。浏览器绝不会把这一段发送给任何服务器。链接会打开本页,直接停在“打开”标签并已填好密钥,随后把密钥从地址栏移除。
文件和密钥请分开发送。
同时握有封存文件和密钥的人,就能打开它。请走不同的渠道发送,比如文件用电子邮件、密钥链接用短信。这样就没有任何一个收件箱同时装着两者。
已从你的链接中读取密钥,并已从地址栏移除,因此不会留在你的浏览记录里。
检查中…
这个封存文件里有什么
目前还没有保存任何东西。以下是这个封存文件自称包含的内容:
- 名称
- 大小
- 类型
当心——这种文件类型可能在你的设备上运行代码
已解密。
文件已在你的设备上解密,并以下载的形式交给你。与它有关的任何内容都不曾离开本页。
来自 Sealby 团队——iPhone 上的加密保险库
Sealby 把照片、文件和笔记存放在你 iPhone 上的加密保险库里。无账户,无服务器,除你之外,无人能读。
每个参数,公开说明
- 加密算法
- AES-256-GCM(WebCrypto),每个分块 128 位标签
- 密钥
- 256 位,取自浏览器 CSPRNG,每次封存新生成,只用一次,不由任何东西派生
- 分块方式
- 先是一份加密的清单(文件名、大小、类型),然后是切成 1 MiB 分块的文件——每一块单独认证
- 随机数
- 32 位随机封存盐值 ‖ 64 位分块计数器——在同一个文件内绝不重复,何况密钥本来就只用一次
- 认证数据
- 完整的 16 字节头部,加上每一块的序号和分块总数——重排、截断或拼接分块都会使解密失败
- 密钥字符串
- base64url,47 个字符:32 字节密钥加上 3 字节 SHA-256 校验和,因此笔误会干净地失败,而不是产生一堆乱码
关于它到底做了什么的诚实回答
一切都在你的浏览器里发生。
封存和打开都会把文件读进本页的内存,在那里用你浏览器内建的 WebCrypto 引擎运行 AES-256-GCM,再把结果以下载的形式交还给你。绝无上传。没有账户,也没有服务器端处理。与本站其余部分不同,本页连那个无 Cookie 的访问计数器都不加载。不含任何形式的分析。
这不只是一句承诺。本页附带一份 Content-Security-Policy 送达,它只允许浏览器获取本站自己的静态文件。它没有任何办法把你的文件、你的密钥或别的什么发送给任何服务器,包括我们的。封存时打开浏览器的开发者工具,你会看到零个请求。要最直白的证明,就装上工具箱,在飞行模式下封存。断着网,什么都无法离开。
密钥就是一切——而我们从未拥有它。
每次封存都会在你的浏览器里生成一把全新的随机 256 位密钥。它只向你显示一次,并且不保存在任何地方:不在我们的系统里(根本没有系统参与),不在你浏览器的存储里,也不在封存文件里。没有可猜的密码短语,也没有可穷举的东西。谁同时握有密钥和文件,谁就能打开它。没有的人,打不开。
这带来一个硬邦邦的后果:密钥丢了,文件就没了。没有重置,没有工单,没有找回。这不是我们在故意为难。一份我们能恢复的副本,就是一份别人可以窃取或索要的副本。
密钥链接永远到不了服务器。
分享链接把密钥放在 URL 中 # 之后。这一段,也就是 URL 的片段部分,留在浏览器里。它绝不包含在浏览器发往任何服务器的内容里,无论是我们的还是别人的。当本页带着链接里的密钥加载时,它会读取密钥、切换到“打开”标签,并立即把密钥从地址栏移除。因此它不会滞留在可见的 URL 或浏览记录中。
不过,同时拿到链接和封存文件的人,仍然能打开它。这正是本页提醒你把文件和密钥走不同渠道发送的原因。
在任何东西被保存之前,你先看清里面是什么。
打开分两步进行。第一步,本页只解密一份很小的清单:原始文件名、大小和类型。它把这些展示给你,对可能运行代码的文件类型(程序、脚本、安装包、带宏的文档、HTML)以及像 invoice.pdf.exe 这样用双扩展名伪装的名称发出醒目的警告。在你点击解密之前,不会写入任何东西。本页从不自行打开或呈现解密后的内容。它只把文件交给你下载。
请分清加密能证明什么、不能证明什么。封存文件在传输途中是保密的。封存本身丝毫不能说明它出自谁手,也不能说明它的内容是否安全。对待一个意料之外的封存文件,请像对待一封意料之外的附件一样保持怀疑。
信任边界究竟画在哪里。
你运行的是本网站送到你手上的代码。如果 sealby.app 或者你通往它的连接被攻陷,一个恶意页面就能读到你放进去的东西。每一个基于网页的加密工具都是如此,我们宁愿说出来,也不愿假装不是这样。我们为此做了这些:本页是一个静态文件,我们让它不含任何第三方代码。安装后的工具箱让你可以继续使用已经加载过的那一版。而且格式在下方有文档,独立的工具可以照着它来实现。
你自己的设备同样在这条边界之内。拥有页面访问权的浏览器扩展,或机器上的恶意软件,能读到任何页面能读到的一切。封存文件隐藏的是它的内容,并非一切。任何拿到它的人都能看出它的大小(大致等于原件的大小),以及它是一个由 Sealby 封存的文件。
用大白话说局限
- 密钥丢了 = 文件没了。没有例外,这是有意的设计。
- 我们看不到你的文件和密钥,因此也无法替你扫描其中的恶意软件。文件类型警告只基于文件名。它不是安全保证。
- 任何同时拥有封存文件和密钥的人都能打开它。本工具既不知道也不核实他们是谁。
- 封存文件的大小会大致透露原文件的大小。
- 每次封存一个文件,最大 100 MB。多个文件请先打包成 zip。
格式,逐字节
技术读者独立验证本页输出所需的一切,或者用三十行 Python 打开一个 .sealed 文件:
| bytes | field | contents |
|---|---|---|
| 0–7 | magic | ASCII "SEALBY-W" |
| 8 | version | 0x01——其他任何值都会以“由更新版本创建”为由被拒绝 |
| 9 | algorithm | 0x01 = AES-256-GCM |
| 10–13 | 密封盐值 | 4 字节,CSPRNG,每次封存新生成 |
| 14–15 | reserved | 必须为零 |
| 16–end | 分块帧 | 每一帧:uint32 大端序的密文长度,然后是 AES-GCM 密文 + 16 字节标签 |
第 0 块是清单:UTF-8 JSON {"name":…,"size":…,"type":…}。第 1…N−1 块是切成 1 MiB 的文件。对 N 块中的第 i 块:nonce = salt ‖ uint64 大端序 i;认证数据 = header ‖ uint64 大端序 i ‖ uint64 大端序 N。因此篡改、截断、重排、挪位以及跨文件拼接都会通不过认证。每一项都有已提交的负面测试为证。
密钥字符串:32 字节密钥后接其 SHA-256 的前 3 字节,整体做 base64url 编码(不带填充),共 47 个字符。笔误会在校验和上失败,而不是产生一堆乱码。
完整的字节级精确规范(FORMAT-WEB-v1) ·已知答案测试向量(JSON)
参考打开器(Python,cryptography 库)
import base64, hashlib, json, struct, sys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
raw = open(sys.argv[1], "rb").read()
assert raw[:8] == b"SEALBY-W" and raw[8] == 1 and raw[9] == 1
assert raw[14:16] == b"\x00\x00"
header, salt = raw[:16], raw[10:14]
frames, off = [], 16
while off < len(raw):
(n,) = struct.unpack(">I", raw[off:off + 4]); off += 4
frames.append(raw[off:off + n]); off += n
assert off == len(raw)
kb = base64.urlsafe_b64decode(sys.argv[2] + "=") # 47-char key string
key, check = kb[:32], kb[32:35]
assert hashlib.sha256(key).digest()[:3] == check # typo check
N = len(frames)
def chunk(i):
nonce = salt + struct.pack(">Q", i)
aad = header + struct.pack(">QQ", i, N)
return AESGCM(key).decrypt(nonce, frames[i], aad)
manifest = json.loads(chunk(0))
data = b"".join(chunk(i) for i in range(1, N))
assert len(data) == manifest["size"]
open(manifest["name"], "wb").write(data)
print("decrypted ->", manifest["name"])这是为路上的文件准备的。保险库是为留下来的文件准备的。
封存保护的是一个正在去往某处的文件。Sealby 保护的是留下来的一切:iPhone 上加密保险库里的照片、文件和笔记。无账户,无服务器,除你之外,无人能读。
iPhone 和 iPad · iOS 17+ · 免费