编码、转义和散列
Base64 不是加密,btoa 不是 UTF-8,encodeURI 不是encodeURIComponent,SHA-256 不是密码哈希。以下是它们的实际作用,以及否则假设会产生的具体错误。
编码不是加密,也不是压缩
编码改变了数据的写入方式。加密改变了谁可以读取它。压缩会改变它占用的空间大小。这是三项不同的工作,而 base64 只完成第一项——如果您希望完成其他两项中的任何一项,那就很糟糕了。
Base64 一次获取三个字节,并将它们重写为从 64 符号字母表中提取的四个字符。四个字符携带三个字节意味着输出始终比输入大 33% 左右,加上填充。它的存在是因为大量基础设施——电子邮件标头、HTTP 标头、JSON 字符串值、URL、XML 属性——都是为文本和破坏或拒绝任意字节而设计的。 Base64 是一种适配器,可让您通过文本形状的管道推送字节。
任何人都可以立即逆转,无需钥匙,因为没有钥匙。如果您对密码进行 Base64 处理,则您已以不太方便的格式发布了密码。这很重要,因为在人眼看来,base64 输出看起来是混乱的,这正是人们信任它做不到的事情的特性。
为什么 btoa() 会中断,以及它中断的两种不同方式
浏览器为您提供 btoa() 和 atob(),它们比现代文本 API 更古老。 btoa 是在“二进制字符串”上定义的:其中每个代码单元都是单个字节的字符串,从 0 到 255。文字不是那样的。
第一次失败是响亮的。调用 btoa("世界") 会得到一个 InvalidCharacterError,因为 U+4E16 不适合一个字节。严重的失败是好的——你会立即注意到它们并寻找解决方案。
第二次故障是无声的,并且是到达生产的故障。字符 é 是 U+00E9,它确实适合一个字节。所以 btoa("café") 愉快地返回,将 é 编码为单字节 0xE9。但是UTF-8中的é是两个字节,0xC3 0xA9。您刚刚生成的 Base64 在地球上的所有其他系统中都会解码为非您文本的内容。几周后,您会发现数据库中的名称已变成替换字符。
解决方法是停止将文本视为字节并显式转换它。 TextEncoder 生成 UTF-8 bytes;对这些进行编码。 TextDecoder 将字节转换回文本,并使用 { fatal: true } 构造它,使其抛出无效序列,而不是悄悄地替换 U+FFFD,因此不可能正确的解码会失败,而不是返回看似合理的废话。这就是该工具包使用的管道,这就是为什么表情符号、标记和从右到左的脚本完全往返的原因。
- 使用 TextEncoder 将文本转换为字节 — 切勿对字符串进行索引。
- 将字节编码为 base64。
- 反转:将 base64 解码为字节,然后使用 fatal: true 将字节解码为 UTF-8。
- 如果 UTF-8 步骤失败,则有效负载是二进制的,而不是文本。将其显示为十六进制而不是假装。
base64 与 base64url 以及填充问题
标准 base64 使用 + 和 / 作为最后两个符号。两者在 URL 中都是有意义的:+ 可以被读取为查询字符串中的编码空格,而 / 是路径分隔符。因此 RFC 4648 定义了第二个字母表,base64url,它用 - 和 _ 代替。 JWT 使用它,大多数令牌格式和许多 API 也是如此。
填充是另一个变量。标准 base64 用 = 填充,因此输出长度始终是四的倍数。 base64url 通常会删除填充,因为 = 本身就是 URL 中的一个尴尬字符,并且可以通过算术恢复长度。坚持填充的解码器将拒绝完全有效的 JWT 片段。
实用建议:您的解码器应该接受两种字母并容忍缺失的填充,因为您很少控制所收到的内容。您的编码器应该明确其发出的内容,因为接收者可能确实关心。这里的 base64 实用程序正是这样做的 - 它接受任何合理的内容,并让您精确选择它生成的内容。
encodeURI 和encodeURIComponent:一句话的区别
两者都使用 UTF-8 进行百分比编码。它们的区别仅在于保留哪些字符,而这种差异就是整个故事:encodeURIComponent 转义保留的分隔符,encodeURI 则不会。
保留的分隔符是赋予 URL 结构的字符: : / ? #[]@! $ & ' ( ) * + , ; =。 encodeURI 假设您向它提供了一个已经正确构造的 URL,并且必须保持这种方式,因此它会保留它们 - 它不会将 https:// 转换为 https%3A%2F%2F。 encodeURIComponent 假设您交给它一个将被放入槽中的片段,因此它会转义它们,确保该片段不会脱离其槽。
这产生的错误完全是机械性的。取搜索值a&b=c。用encodeURI对其进行编码,并将其附加为?q=a&b=c,您就默默地创建了两个参数:q现在只是“a”,并且出现了一个杂散的b=c。使用encodeURIComponent对其进行编码,您将得到?q=a%26b%3Dc,一个参数,正确的值。同一类错误可以让精心设计的值将参数注入到代码构建的 URL 中,这就是为什么“使用组件形式的值”是一条安全规则,而不仅仅是正确性规则。
表单编码是第三条规则,与第二条规则类似。 application/x-www-form-urlencoded 将空格写入 + 而不是 %20。如果您使用普通的decodeURIComponent 解码表单主体,则数据中的每个加号都会变成空格。每个曾经将“C++”改成“C”的搜索框都是这个错误。
HTML 实体,以及为什么使用 innerHTML 解码它们是一个坏习惯
HTML 的转义范围很窄并且很好理解:& 变成 &,< 变成 <,> 变成 >,并且内部属性值 " 和 ' 也需要转义。五个字符。转义更多字符——将每个重音字母变成命名实体——是字符编码不确定的时代的一种解决方法,现在是可选样式而不是安全性。
解码是坏习惯的根源。每个答案中出现的一行技巧是将字符串分配给分离元素的innerHTML并读回其textContent。它有效,但这是一个糟糕的主意。您已将不受信任的输入传递给 HTML 解析器,该解析器会从中构建真实的 DOM 节点。该字符串中的 <img src=x onerror=...> 成为附加了实际错误处理程序的实际图像元素;如果该子树被插入到文档中,它就会运行。它还会默默地破坏您的数据:输入中的标签消失而不是往返,因为解析器将它们解释为标记而不是文本。
正确解码实体根本不需要解析器:匹配引用、在表中查找名称或对数字引用进行算术。那是几十行,它不能执行任何东西,并且它忠实地往返。该工具包就是这样做的,这就是为什么将脚本标记粘贴到实体解码器中会显示脚本标记。
选择哈希,以及决定它的三个问题
加密哈希将任何输入转换为固定长度的摘要,因此找到具有相同摘要的两个输入应该是不可行的。该属性让摘要代表数据——在签名、完整性检查或内容地址中。
第一个问题:您是在防范事故还是防范对手?防止下载损坏的校验和只需要捕获随机翻转即可; CRC32 没问题。攻击者可以从碰撞中受益的摘要需要仍然有效的哈希值。这种区别就是 SHA-1 不仅仅是“旧”的原因。
具体来说,SHA-1 已损坏。在 2017 中,SHAttered 工作生成了两个具有相同 SHA-1 摘要的不同 PDF 文件。在 2020 中,“SHA-1 is a Shambles”演示了选择前缀冲突 - 更强大且更危险的变体,因为它让攻击者可以碰撞两个有意义的不同文档,而不是两个精心构造的 blob。如果系统的安全性依赖于 SHA-1 抗碰撞性,那么这种安全性就消失了。 SHA-1 保留在该工具包中,因为 git 对象 ID 和一长串遗留 API 签名仍然使用它,并且您需要能够重现这些值。复制价值与依赖它不同。
第二个问题:输入的是密码吗?如果是这样,那么这些都不是答案。 SHA-256 的设计目标是快速,而快速对于密码来说恰恰是错误的:这意味着使用您的数据库的攻击者每秒可以尝试数十亿次猜测。密码需要一个故意缓慢、内存困难的函数,并带有每个用户的盐——Argon2id、scrypt 或 bcrypt。这不是一个细微差别;而是。使用 SHA-256 作为密码是最常见的严重哈希错误。
第三个问题:您需要密钥摘要吗?如果您要对消息进行身份验证而不是对其进行指纹识别,则需要 HMAC,而不是裸露的哈希值。连接秘密并对其进行散列处理是对抗长度扩展攻击的经典自身目标; HMAC 之所以存在,是因为这种结构比看起来更难实现。
对于其他所有内容 - 对文件、内容地址、完整性属性进行指纹识别 - SHA-256 是合理的默认值,SHA-512 在 64 位硬件上通常更快,同时提供更广泛的摘要。
为什么这里的哈希值来自浏览器
该工具包中的摘要由浏览器自己的 Web Crypto 实现 SubtleCrypto 计算,而不是由该站点提供的 JavaScript 计算。这是一个深思熟虑的选择:浏览器的实现经过审核、维护,并且通常作为优化的本机代码运行。页面束中手写的 SHA-256 是更多值得信任的代码,但没有任何好处。
它有一个明显的后果。 Web Crypto 仅在安全上下文中公开,即 https:// 或 localhost。在 LAN 地址上通过纯 HTTP 打开此页面,crypto.subtle 将是未定义的,因此哈希实用程序会清楚地告诉您,而不是默默地失败或替换更弱的东西。
同样的推理驱动 UUID 生成器。 crypto.randomUUID() 也是仅安全上下文的,因此在它不可用的情况下,工具包会回退到 crypto.getRandomValues() (它仍然是相同的加密安全源)并自行设置版本和变体位。它永远不会做的是回退到 Math.random()。这是一种快速的非加密 PRNG,其内部状态可以从其短期输出中恢复,并且标识符有一个不幸的习惯,即被提升为会话密钥和密码重置链接。如果不存在安全来源,则该工具不会生成任何内容并说明原因。
你粘贴的内容会发生什么
- 每个转换、哈希、解码和差异都在您的浏览器选项卡中运行。不会在服务器上上传、记录或存储任何输入,因为页面加载后就不再涉及服务器。
- 哈希值来自浏览器自己的 Web Crypto 实现,UUID 来自其加密安全随机生成器。两者都不涉及网络调用。
- 您键入的任何内容都不会写入本地存储或 cookie。重新加载页面会丢弃它;关闭选项卡将丢弃它。
- 站点范围的分析仅在配置的规范生产主机上运行,并在隐私政策中披露;本地和预览主机拒绝它。粘贴的值、令牌、URL 和文件内容不包括在 ToolAcre 自己的分析事件中。当前配置中禁用广告。
- 也就是说:JWT 或 API 密钥是实时凭证。安全的习惯是永远不要将其粘贴到不是您编写的网页中,无论其声明多么可信 - 包括这个。
问题
Base64 是一种隐藏数据的方法吗?
不。它是一种没有密钥的可逆文本表示形式,任何人都可以在几分之一秒内解码。它使数据能够在纯文本渠道中生存;它并没有使它成为秘密。任何真正敏感的内容都需要加密,而加密结果通常会进行 Base64 编码以进行传输——这就是混乱的根源。
为什么我的 Base64 比输入长?
由于四个输出字符携带三个输入字节,因此输出的大小大致为 4/3 ,加上最多两个填充字符。这是格式所固有的。如果大小很重要,请在编码之前进行压缩,而不要在编码之后进行压缩,因为 base64 输出的压缩效果很差。
我应该使用哪种 URL 编码函数?
对要插入 URL 的任何单个片段使用encodeURIComponent:查询值、路径段、片段。仅当您拥有仅包含空格或非 ASCII 的完整、已结构化的 URL 时,才使用encodeURI。如果您正在构建查询字符串,请首选 URLSearchParams,它会为您应用正确的规则并处理空格加号差异。
为什么我的解码器会抛出“URI 格式错误”?
因为输入中的 % 后面没有跟两个十六进制数字。通常,文本包含一个字面百分号——“50% off”——从未被编码。文字百分比必须写为 %25。这里的 URL 实用程序报告有问题的转义的确切位置,而不是仅仅拒绝。
我可以使用 SHA-256 来存储密码吗?
不会。SHA-256 的设计速度很快,这意味着窃取您数据库的攻击者每秒可以在商用硬件上测试数十亿个候选密码。密码需要一个缓慢、难以记忆的加盐函数:Argon2id、scrypt 或 bcrypt。这是该领域最常见的严重错误。
如果 SHA-1 损坏了,为什么它还在这里?
因为您仍然需要重现已存在的 SHA-1 值:git 对象 ID、旧的 TLS 证书指纹、旧版 API 请求签名。能够计算互操作性的值与依赖它来保证安全性是不同的。此工具包中出现 SHA-1 的每个位置都有相应的标签。
为什么两个工具对同一文本给出不同的哈希值?
几乎总是字节的差异,而不是算法的差异。通常的罪魁祸首是尾随换行符(文件以换行符结尾;文本框可能不是)、不同的文本编码或 CRLF 与 LF 行结尾。该工具对您键入的内容的 UTF-8 bytes 进行哈希处理,并显示字节数,这通常会使差异变得明显。
局限性
- 命名的 HTML 实体表涵盖了实际子集 - 标记关键字符、版式、货币、箭头、数学、希腊语和拉丁语 - 1 - 并非所有 2,231 HTML5 命名引用。无法识别的名字会被报告并完全按照书写内容而不是猜测。
- 实体解码需要终止分号。 HTML5 可以容忍少量遗留引用,但正确解码它们取决于周围的标记上下文,这是独立文本工具所不具备的。
- 哈希和 UUID 生成需要安全上下文(https:// 或 localhost),因为否则 Web Crypto 不会暴露。该工具会报告这一点,而不是替换较弱的实现。
- 只有 SHA-1、SHA-256、SHA-384 和 SHA-512 可用,因为这些是 SubtleCrypto 实现的。 MD5 的缺失是出于选择,也是出于必然。
- 这里没有 HMAC,没有密钥派生,也没有加密。这些需要密钥管理,这不是您在互联网上找到的页面应该处理的事情。
- 一切都受到设备内存的限制,因为所有内容都在一个浏览器选项卡中运行。输入是有上限的——每个实用程序只有几兆字节——并且该工具拒绝超大工作而不是冻结。