2025-04-10 日报 Day152

2025-04-10 日报 Day152

Yuyang 前端小白🥬

今日的鸡汤

一寸光阴一寸金,寸金难买寸光阴,时间买不到也租不来,惜时如金就是增进财富储量,因时制宜就是掌控发展变量,分秒必争就是创造价值增量。

今日学习内容

1、JS 红皮书 P663-674 第二十章:JavaScript API

今日笔记

1、 Web Cryptography API: Web Cryptography API 描述了一套密码学工具,规范了 JavaScript 如何以安全和符合惯例的方式实现加密。这些工具包括生成、使用和应用加密密钥对,加密和解密消息,以及可靠地生成随机数。
2、生成随机数: 在需要生成随机值时,很多人会使用 Math.random()。这个方法在浏览器中是以伪随机数生成器(PRNG,PseudoRandom Number Generator)方式实现的。所谓“伪”指的是生成值的过程不是真的随机。PRNG 生成的值只是模拟了随机的特性。
由于算法本身是固定的,其输入只是之前的状态,因此随机数顺序也是确定的。xorshift128+使用 128 位内部状态,而算法的设计让任何初始状态在重复自身之前都会产生 2128–1 个伪随机值。这种循环被称为置换循环(permutation cycle),而这个循环的长度被称为一个周期(period)。很明显,如果攻击者知道 PRNG 的内部状态,就可以预测后续生成的伪随机值。如果开发者无意中使用 PRNG 生成了私有密钥用于加密,则攻击者就可以利用 PRNG 的这个特性算出私有密钥。
伪随机数生成器主要用于快速计算出看起来随机的值。不过并不适合用于加密计算。为解决这个问题,密码学安全伪随机数生成器(CSPRNG,Cryptographically Secure PseudoRandom Number Generator)额外增加了一个熵作为输入,例如测试硬件时间或其他无法预计行为的系统特性。这样一来,计算速度明显比常规 PRNG 慢很多,但 CSPRNG 生成的值就很难预测,可以用于加密了。
Web Cryptography API 引入了 CSPRNG,这个 CSPRNG 可以通过 crypto.getRandomValues()在全局 Crypto 对象上访问。

1
2
3
4
5
6
7
8
9
const array = new Uint8Array(1);
for (let i = 0; i < 5; ++i) {
console.log(crypto.getRandomValues(array));
}
// Uint8Array [41]
// Uint8Array [250]
// Uint8Array [51]
// Uint8Array [129]
// Uint8Array [35]

要使用 CSPRNG 重新实现 Math.random(),可以通过生成一个随机的 32 位数值,然后用它去除最大的可能值 0xFFFFFFFF。这样就会得到一个介于 0 和 1 之间的值

1
2
3
4
5
6
7
8
9
function randomFloat() {
// 生成 32 位随机值
const fooArray = new Uint32Array(1);
// 最大值是 2^32 –1
const maxUint32 = 0xffffffff;
// 用最大可能的值来除
return crypto.getRandomValues(fooArray)[0] / maxUint32;
}
console.log(randomFloat()); // 0.5033651619458955

3、使用 SubtleCrypto 对象: Web Cryptography API 重头特性都暴露在了 SubtleCrypto 对象上,可以通过 window.crypto.subtle 访问:
console.log(crypto.subtle); // SubtleCrypto {}
这个对象包含一组方法,用于执行常见的密码学功能,如加密、散列、签名和生成密钥。

  • 生成密码学摘要: 计算数据的密码学摘要是非常常用的密码学操作。这个规范支持 4 种摘要算法:SHA-1 和 3 种 SHA-2。
     SHA-1(Secure Hash Algorithm 1):架构类似 MD5 的散列函数。接收任意大小的输入,生成 160 位消息散列。由于容易受到碰撞攻击,这个算法已经不再安全。
     SHA-2(Secure Hash Algorithm 2):构建于相同耐碰撞单向压缩函数之上的一套散列函数。规范支持其中 3 种:SHA-256、SHA-384 和 SHA-512。生成的消息摘要可以是 256 位(SHA-256)、384 位(SHA-384)或 512 位(SHA-512)。这个算法被认为是安全的,广泛应用于很多领域和协议,包括 TLS、PGP 和加密货币(如比特币)。
    SubtleCrypto.digest()方法用于生成消息摘要。要使用的散列算法通过字符串”SHA-1”、”SHA-256”、”SHA-384”或”SHA-512”指定。下面的代码展示了一个使用 SHA-256 为字符串”foo”生成消息摘要的例子:
1
2
3
4
5
6
7
8
(async function () {
const textEncoder = new TextEncoder();
const message = textEncoder.encode("foo");
const messageDigest = await crypto.subtle.digest("SHA-256", message);
console.log(new Uint32Array(messageDigest));
})();
// Uint32Array(8) [1806968364, 2412183400, 1011194873, 876687389,
// 1882014227, 2696905572, 2287897337, 2934400610]

通常,在使用时,二进制的消息摘要会转换为十六进制字符串格式。通过将二进制数据按 8 位进行分割,然后再调用 toString(16)就可以把任何数组缓冲区转换为十六进制字符串:

1
2
3
4
5
6
7
8
9
10
(async function () {
const textEncoder = new TextEncoder();
const message = textEncoder.encode("foo");
const messageDigest = await crypto.subtle.digest("SHA-256", message);
const hexDigest = Array.from(new Uint8Array(messageDigest))
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
console.log(hexDigest);
})();
// 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae

软件公司通常会公开自己软件二进制安装包的摘要,以便用户验证自己下载到的确实是该公司发布的版本(而不是被恶意软件篡改过的版本)。下面的例子演示了下载 Firefox v67.0,通过 SHA-512 计算其散列,再下载其 SHA-512 二进制验证摘要,最后检查两个十六进制字符串匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(async function () {
const mozillaCdnUrl =
"// downloadorigin.cdn.mozilla.net/pub/firefox/releases/67.0 /";
const firefoxBinaryFilename = "linux-x86_64/en-US/firefox-67.0.tar.bz2";
const firefoxShaFilename = "SHA512SUMS";
console.log("Fetching Firefox binary...");
const fileArrayBuffer = await (
await fetch(mozillaCdnUrl + firefoxBinaryFilename)
).arrayBuffer();
console.log("Calculating Firefox digest...");
const firefoxBinaryDigest = await crypto.subtle.digest(
"SHA-512",
fileArrayBuffer
);
const firefoxHexDigest = Array.from(new Uint8Array(firefoxBinaryDigest))
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
console.log("Fetching published binary digests...");
// SHA 文件包含此次发布的所有 Firefox 二进制文件的摘要,
// 因此要根据其格式进制拆分
const shaPairs = (
await (await fetch(mozillaCdnUrl + firefoxShaFilename)).text()
)
.split(/\n/)
.map((x) => x.split(/\s+/));
let verified = false;
console.log("Checking calculated digest against published digests...");
for (const [sha, filename] of shaPairs) {
if (filename === firefoxBinaryFilename) {
if (sha === firefoxHexDigest) {
verified = true;
break;
}
}
}
console.log("Verified:", verified);
})();
// Fetching Firefox binary...
// Calculating Firefox digest...
// Fetching published binary digests...
// Checking calculated digest against published digests...
// Verified: true
  • CryptoKey与算法: 如果没了密钥,那密码学也就没什么意义了。SubtleCrypto 对象使用 CryptoKey 类的实例来生成密钥。CryptoKey 类支持多种加密算法,允许控制密钥抽取和使用。
    CryptoKey 类支持以下算法,按各自的父密码系统归类。
     RSA(Rivest-Shamir-Adleman):公钥密码系统,使用两个大素数获得一对公钥和私钥,可用于签名/验证或加密/解密消息。RSA 的陷门函数被称为分解难题(factoring problem)。
     RSASSA-PKCS1-v1_5:RSA 的一个应用,用于使用私钥给消息签名,允许使用公钥验证签名。
     SSA(Signature Schemes with Appendix),表示算法支持签名生成和验证操作。
     PKCS1(Public-Key Cryptography Standards #1),表示算法展示出的 RSA 密钥必需的数学特性。
     RSASSA-PKCS1-v1_5 是确定性的,意味着同样的消息和密钥每次都会生成相同的签名。
     RSA-PSS:RSA 的另一个应用,用于签名和验证消息。
     PSS(Probabilistic Signature Scheme),表示生成签名时会加盐以得到随机签名。
     与 RSASSA-PKCS1-v1_5 不同,同样的消息和密钥每次都会生成不同的签名。
     与 RSASSA-PKCS1-v1_5 不同,RSA-PSS 有可能约简到 RSA 分解难题的难度。
     通常,虽然 RSASSA-PKCS1-v1_5 仍被认为是安全的,但 RSA-PSS 应该用于代替RSASSA-PKCS1-v1_5。
     RSA-OAEP:RSA 的一个应用,用于使用公钥加密消息,用私钥来解密。
     OAEP(Optimal Asymmetric Encryption Padding),表示算法利用了 Feistel 网络在加密前处理未加密的消息。
     OAEP 主要将确定性 RSA 加密模式转换为概率性加密模式。
     ECC(Elliptic-Curve Cryptography):公钥密码系统,使用一个素数和一个椭圆曲线获得一对公钥和私钥,可用于签名/验证消息。ECC 的陷门函数被称为椭圆曲线离散对数问题(elliptic curve discrete logarithm problem)。ECC 被认为优于 RSA。虽然 RSA 和 ECC 在密码学意义上都很强,但 ECC 密钥比 RSA 密钥短,而且 ECC 密码学操作比 RSA 操作快。
     ECDSA(Elliptic Curve Digital Signature Algorithm):ECC 的一个应用,用于签名和验证消息。这个算法是数字签名算法(DSA,Digital Signature Algorithm)的一个椭圆曲线风格的变体。
     ECDH(Elliptic Curve Diffie-Hellman):ECC 的密钥生成和密钥协商应用,允许两方通过公开通信渠道建立共享的机密。这个算法是 Diffie-Hellman 密钥交换(DH,Diffie-Hellman key exchange)协议的一个椭圆曲线风格的变体。
     AES(Advanced Encryption Standard):对称密钥密码系统,使用派生自置换组合网络的分组密码加密和解密数据。AES 在不同模式下使用,不同模式算法的特性也不同。
     AES-CTR:AES 的计数器模式(counter mode)。这个模式使用递增计数器生成其密钥流,其行为类似密文流。使用时必须为其提供一个随机数,用作初始化向量。AES-CTR 加密/解密可以并行。
     AES-CBC:AES 的密码分组链模式(cipher block chaining mode)。在加密纯文本的每个分组之前,先使用之前密文分组求 XOR,也就是名字中的“链”。使用一个初始化向量作为第一个分组的 XOR 输入。
     AES-GCM:AES 的伽罗瓦/计数器模式(Galois/Counter mode)。这个模式使用计数器和初始化向量生成一个值,这个值会与每个分组的纯文本计算 XOR。与 CBC 不同,这个模式的 XOR 输入不依赖之前分组密文。因此 GCM 模式可以并行。由于其卓越的性能,AES-GCM 在很多网络安全协议中得到了应用。
     AES-KW:AES 的密钥包装模式(key wrapping mode)。这个算法将加密密钥包装为一个可移植且加密的格式,可以在不信任的渠道中传输。传输之后,接收方可以解包密钥。与其他 AES 模式不同,AES-KW 不需要初始化向量。
     HMAC(Hash-Based Message Authentication Code):用于生成消息认证码的算法,用于验证通过不可信网络接收的消息没有被修改过。两方使用散列函数和共享私钥来签名和验证消息。
     KDF(Key Derivation Functions):可以使用散列函数从主密钥获得一个或多个密钥的算法。KDF能够生成不同长度的密钥,也能把密钥转换为不同格式。
     HKDF(HMAC-Based Key Derivation Function):密钥推导函数,与高熵输入(如已有密钥)一起使用。
     PBKDF2(Password-Based Key Derivation Function 2):密钥推导函数,与低熵输入(如密钥字符串)一起使用。
  • 生成CryptoKey: 使用 SubtleCrypto.generateKey()方法可以生成随机 CryptoKey,这个方法返回一个期约,解决为一个或多个 CryptoKey 实例。使用时需要给这个方法传入一个指定目标算法的参数对象、一个表示密钥是否可以从 CryptoKey 对象中提取出来的布尔值,以及一个表示这个密钥可以与哪个SubtleCrypto 方法一起使用的字符串数组(keyUsages)。
    -生成CryptoKey: 使用 SubtleCrypto.generateKey()方法可以生成随机 CryptoKey,这个方法返回一个期约,解决为一个或多个 CryptoKey 实例。使用时需要给这个方法传入一个指定目标算法的参数对象、一个表示密钥是否可以从 CryptoKey 对象中提取出来的布尔值,以及一个表示这个密钥可以与哪个SubtleCrypto 方法一起使用的字符串数组(keyUsages)。
    由于不同的密码系统需要不同的输入来生成密钥,上述参数对象为每种密码系统都规定了必需的输入:
     RSA 密码系统使用 RsaHashedKeyGenParams 对象;
     ECC 密码系统使用 EcKeyGenParams 对象;
     HMAC 密码系统使用 HmacKeyGenParams 对象;
     AES 密码系统使用 AesKeyGenParams 对象。
    keyUsages 对象用于说明密钥可以与哪个算法一起使用。至少要包含下列中的一个字符串:
     encrypt
     decrypt
     sign
     verify
     deriveKey
     deriveBits
     wrapKey
     unwrapKey
    keyUsages 对象用于说明密钥可以与哪个算法一起使用。至少要包含下列中的一个字符串:
     encrypt
     decrypt
     sign
     verify
     deriveKey
     deriveBits
     wrapKey
     unwrapKey
    假设要生成一个满足如下条件的对称密钥:
     支持 AES-CTR 算法;
     密钥长度 128 位;
     不能从 CryptoKey 对象中提取;
     可以跟 encrypt()和 decrypt()方法一起使用。
    那么可以参考如下代码:
    (async function() {
    const params = {
    name: ‘AES-CTR’,
    length: 128
    };
    const keyUsages = [‘encrypt’, ‘decrypt’];
    const key = await crypto.subtle.generateKey(params, false, keyUsages);
    console.log(key);
    // CryptoKey {type: “secret”, extractable: true, algorithm: {…}, usages: Array(2)}
    })();
  • 导出和导入密钥: 如果密钥是可提取的,那么就可以在 CryptoKey 对象内部暴露密钥原始的二进制内容。使用exportKey()方法并指定目标格式(”raw”、”pkcs8”、”spki”或”jwk”)就可以取得密钥。这个方法返回一个期约,解决后的 ArrayBuffer 中包含密钥:
    (async function() {
    const params = {
    name: ‘AES-CTR’,
    length: 128
    };
    const keyUsages = [‘encrypt’, ‘decrypt’];
    const key = await crypto.subtle.generateKey(params, true, keyUsages);
    const rawKey = await crypto.subtle.exportKey(‘raw’, key);
    console.log(new Uint8Array(rawKey));
    // Uint8Array[93, 122, 66, 135, 144, 182, 119, 196, 234, 73, 84, 7, 139, 43, 238,
    // 110]
    })();
    exportKey()相反的操作要使用 importKey()方法实现。importKey()方法的签名实际上是generateKey()和 exportKey()的组合。下面的方法会生成密钥、导出密钥,然后再导入密钥:
  • 从主密钥派生密钥: 使用 SubtleCrypto 对象可以通过可配置的属性从已有密钥获得新密钥。
  • 使用非对称密钥签名和验证消息: 通过 SubtleCrypto 对象可以使用公钥算法用私钥生成签名,或者用公钥验证签名。这两种操作分别通过 SubtleCrypto.sign()和 SubtleCrypto.verify()方法完成。
  • 使用对称密钥加密和解密: SubtleCrypto 对象支持使用公钥和对称算法加密和解密消息。这两种操作分别通过 SubtleCrypto.encrypt()和 SubtleCrypto.decrypt()方法完成。加密消息需要传入参数对象以指定算法和必要的值、加密密钥和要加密的数据。
  • 包装和解包密钥: SubtleCrypto 对象支持包装和解包密钥,以便在非信任渠道传输。这两种操作分别通过 SubtleCrypto.wrapKey()和 SubtleCrypto.unwrapKey()方法完成。
    4、小结: 除了定义新标签,HTML5 还定义了一些 JavaScript API。这些 API 可以为开发者提供更便捷的 Web接口,暴露堪比桌面应用的能力。本章主要介绍了以下 API。
     Atomics API 用于保护代码在多线程内存访问模式下不发生资源争用。
     postMessage() API 支持从不同源跨文档发送消息,同时保证安全和遵循同源策略。
     Encoding API 用于实现字符串与缓冲区之间的无缝转换(越来越常见的操作)。
     File API 提供了发送、接收和读取大型二进制对象的可靠工具。
     媒体元素
此页目录
2025-04-10 日报 Day152