Web Cryptography 更新

密码学是信息安全的基石,涵盖数据机密性、数据完整性、身份验证和不可否认性等各个方面。这些为当今互联网的基础技术(如 HTTPS、DNSSEC 和 VPN)提供了支持。WebCrypto API 的创建旨在将这些重要的、高级的密码学能力引入到 Web。该 API 提供了一组 JavaScript 函数,用于操作底层加密操作,例如哈希、签名生成和验证、加密和解密以及共享密钥派生。此外,它还支持相应密钥材料的生成和管理。WebCrypto API 结合了对各种密码操作的完整支持和广泛的算法范围,能够帮助 Web 开发者应对各种安全需求。

这篇博客文章首先讨论了通过原生 API 实现 Web Cryptography 的优势,然后介绍了 WebCrypto API 本身概览。接下来,它展示了更新后的 SubtleCrypto 接口与旧的 webkit- 前缀接口之间的一些差异。文章讨论了一些新增算法,最后演示了如何从 webkit- 前缀 API 平滑过渡到新的、符合标准的 API。

原生还是非原生?

早在 WebCrypto API 标准化之前,就已经创建了一些 JavaScript 密码学库,并且自那时以来一直在开放 Web 中成功服务。那么,为什么还要费心去实现一个基于原生 API 构建的面向 Web 的密码学库呢?原因有几个,其中一个更重要的原因是性能。数字说明真相。我们进行了一些性能测试,以比较我们更新后的 WebCrypto API 和一些著名的纯 JavaScript 实现。

我们选择了最新的 SJCL (1.0.7)、asmcrypto.jsCryptoJS (3.1) 进行比较。测试套件包括:

  1. AES-GCM:测试对一个 4MB 文件的加密/解密,重复一定次数并记录平均速度。使用 256 位 AES 密钥。
  2. SHA-2:使用 SHA-512 对一个 512KB 文件进行哈希计算,重复一定次数并记录平均速度。
  3. RSA:测试对一个 512KB 文件的 RSA-PSS 签名和验证,重复一定次数并记录平均速度。使用 2048 位密钥对和 SHA-512 进行哈希计算。

测试内容经过精心选择,以反映日常最常用的加密操作,并搭配适当的算法。测试平台是一台 MacBook Pro (MacBookPro11,5),配备 2.8 GHz Intel Core i7 处理器,运行 MacOS 10.13 Beta (17A306f) 和 Safari Technology Preview 35。一些纯 JavaScript 实现不支持所有测试内容,因此相关结果已从这些结果中省略。

以下是测试结果。

AES-GCM Encryption/Decryption SHA-2
RSA

如您所见,性能差异巨大。这是一个令人惊讶的结果,因为大多数现代 JavaScript 引擎都非常高效。与我们的 JavaScriptCore 团队合作后,我们了解到这些纯 JavaScript 实现性能不佳的原因在于,其中大多数并未积极维护。很少有实现充分利用我们快速的 JavaScriptCore 引擎或现代 JavaScript 编码实践。否则,差距可能不会那么大。

除了卓越的性能,WebCrypto API 还受益于更好的安全模型。例如,在使用纯 JavaScript 加密库进行开发时,密钥或私钥通常存储在全局 JavaScript 执行上下文中。这极其脆弱,因为密钥会暴露给加载的任何 JavaScript 资源,从而允许 XSS 攻击者窃取密钥。而 WebCrypto API 则通过将密钥或私钥完全存储在 JavaScript 执行上下文之外来保护它们。这限制了私钥被泄露的风险,并减少了攻击者在受害者浏览器中执行 JavaScript 时的被入侵窗口。更重要的是,我们在 macOS/iOS 上的 WebCrypto 实现基于 CommonCrypto 例程,这些例程针对我们的硬件平台进行了高度优化,并定期进行安全和正确性审计和审查。因此,WebCrypto API 是确保用户享受最高安全保护的最佳方式。

WebCrypto API 概述

WebCrypto API 从全局对象 crypto 开始

Crypto
{
    subtle: SubtleCrypto,
    ArrayBufferView getRandomValues(ArrayBufferView array)
}

在其中,它拥有一个 subtle 对象,它是 SubtleCrypto 接口的单例。该接口命名为 subtle 是因为它警告开发人员,许多加密算法具有复杂的使用要求,必须严格遵守才能获得预期的算法安全保证。subtle 对象是与底层加密原语交互的主要入口点。crypto 全局对象还具有 getRandomValues 函数,该函数提供了一个密码学上强大的随机数生成器 (RNG)。WebKit 的 RNG (macOS/iOS) 基于 AES-CTR。

subtle 对象由多种方法组成,以满足底层加密操作的需求

SubtleCrypto
{
    Promise<ArrayBuffer> encrypt(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);
    Promise<ArrayBuffer> decrypt(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);
    Promise<ArrayBuffer> sign(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource data);
    Promise<boolean> verify(AlgorithmIdentifier algorithm, CryptoKey key, BufferSource signature, BufferSource data);
    Promise<ArrayBuffer> digest(AlgorithmIdentifier algorithm, BufferSource data);
    Promise<CryptoKey or CryptoKeyPair> generateKey(AlgorithmIdentifier algorithm, boolean extractable, sequence<KeyUsage> keyUsages );
    Promise<CryptoKey> deriveKey(AlgorithmIdentifier algorithm, CryptoKey baseKey, AlgorithmIdentifier derivedKeyType, boolean extractable, sequence<KeyUsage> keyUsages );
    Promise<ArrayBuffer> deriveBits(AlgorithmIdentifier algorithm, CryptoKey baseKey, unsigned long length);
    Promise<CryptoKey> importKey(KeyFormat format, (BufferSource or JsonWebKey) keyData, AlgorithmIdentifier algorithm, boolean extractable, sequence<KeyUsage> keyUsages );
    Promise<ArrayBuffer> exportKey(KeyFormat format, CryptoKey key);
    Promise<ArrayBuffer> wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm);
    Promise<CryptoKey> unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence<KeyUsage> keyUsages );
}

顾名思义,这些方法表明 WebCrypto API 支持哈希、签名生成和验证、加密和解密、共享密钥派生以及相应的密钥材料管理。让我们仔细看看其中一种方法

Promise<ArrayBuffer> encrypt(AlgorithmIdentifier algorithm,
                             CryptoKey key,
                             BufferSource data)

所有函数都返回一个 Promise,并且大多数函数接受一个 AlgorithmIdentifier 参数。AlgorithmIdentifier 可以是一个指定算法的字符串,也可以是一个包含特定操作所有输入的字典。例如,为了进行 AES-CBC 加密,必须为上述 encrypt 方法提供

var aesCbcParams = {name: "aes-cbc", iv: asciiToUint8Array("jnOw99oOZFLIEPMr")}

CryptoKey 是 WebCrypto API 中密钥材料的抽象。以下是图示

CryptoKey
{
    type: "secret",
    extractable: true,
    algorithm: { name: "AES-CBC", length: 128 },
    usages: ["decrypt", "encrypt"]
}

这段代码告诉我们,该密钥是一个可提取(到 JavaScript 执行上下文)的 AES-CBC“secret”(对称)密钥,长度为 128 位,可用于加密和解密。algorithm 对象是一个描述不同密钥材料的字典,而所有其他槽位是通用的。请记住,CryptoKey 不会将底层密钥数据直接暴露给网页。WebCrypto 的这种设计将 secret 和 private 密钥数据安全地保存在浏览器代理中,同时仍允许 Web 开发者灵活地使用具体密钥。

WebKitSubtleCrypto 的变化

那些从未听说过 WebKitSubtleCrypto 的人可以跳过本节,只使用 SubtleCrypto。本节旨在为当前的 WebKitSubtleCrypto 用户提供令人信服的理由,让他们切换到我们新的符合标准的 SubtleCrypto

1. 符合标准的实现

SubtleCrypto 是当前规范的符合标准的实现,并且与 WebKitSubtleCrypto 完全独立。以下是演示两种 API 在导入 JsonWebKey (JWK) 格式密钥时差异的代码片段示例

var jwkKey = {
    "kty": "oct",  
    "alg": "A128CBC",
    "use": "enc",
    "ext": true,
    "k": "YWJjZGVmZ2gxMjM0NTY3OA"
};

// WebKitSubtleCrypto:
// asciiToUint8Array() takes a string and converts it to an Uint8Array object.
var jwkKeyAsArrayBuffer = asciiToUint8Array(JSON.stringify(jwkKey));
crypto.webkitSubtle.importKey("jwk", jwkKeyAsArrayBuffer, null, false, ["encrypt"]).then(function(key) {
    console.log("An AES-CBC key is imported via JWK format.");
});

// SubtleCrypto:
crypto.subtle.importKey("jwk", jwkKey, "aes-cbc", false, ["encrypt"]).then(function(key) {
    console.log("An AES-CBC key is imported via JWK format.");
});

使用新接口,不再需要将 JSON 密钥转换为 UInt8Array。SubtleCrypto 接口确实比我们旧的 WebKitSubtleCrypto 实现显著更符合标准。以下是运行 W3C WebCrypto API 测试的结果

W3C WebCrypto TestSuite Result Chart
此测试套件是基于最新的 web-platform-tests GitHub 仓库改进的。所有改进都已提交 pull requests:#6100#6101#6102

新实现的覆盖率约为 95%,比我们带有 webkit- 前缀的实现高出 48 倍!所有选定方的具体数字是:带前缀的 WebKit 为 999,Safari 11 为 46653,Chrome 59 为 45709,FireFox 54 为 18636。

2. 支持使用 DER 编码导入和导出非对称密钥

WebCrypto API 规范支持将公钥编码为 SPKI 的 DER 编码,将私钥编码为 PKCS8 的 DER 编码。在此之前,WebKitSubtleCrypto 仅支持 RSA 密钥的基于 JSON 的 JWK 格式。由于其结构和可读性,当密钥在 Web 上使用时,这很方便。然而,当公钥经常在服务器和 Web 浏览器之间交换时,它们通常嵌入在证书中,采用二进制格式。尽管已经编写了一些 JavaScript 框架来读取证书的二进制格式并提取其公钥,但很少有框架将二进制公钥转换为其等效的 JWK 格式。这就是支持 SPKI 和 PKCS8 有用的原因。以下代码片段演示了使用 SubtleCrypto API 可以做什么

// Import:
// Generated from OpenSSL
// Base64URL.parse() takes a Base64 encoded string and converts it to an Uint8Array object.
var spkiKey = Base64URL.parse("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwCjRCtFwvSNYMZ07u5SxARxglJl75T7bUZXFsDVxHkMhpNC2RaN4jWE5bwYUDMeD2fVmxhpaUQn/6AbFLh6gHxtwrCfc7rIo/SfDdGd3GkRlXK5xXwGuM6MvP9nuZHaarIyArRFh2U2UZxFlVsKI0pSHo6n58W1fPZ1syOoVEZ/WYE6gLhMMwfpeAm97mro7mekRdMULOV/mR5Ul3CHm9Zt93Dc8GpnPA8bhLiB0VNyGTEMa06nJul4gj1sjxLDoUvZY2EWq7oUUnfLBUYMfiqK0kQcW94wvBrIq2DQUApLyTTbaAOY46TLwX6c8LtubJriYKTC5a9Bb0/7ovTWB0wIDAQAB");
crypto.subtle.importKey("spki", spkiKey, {name: "RSA-OAEP", hash: "sha-256"}, true, ["encrypt"]).then(function(key) {
    console.log("A RSA-OAEP key is imported via SPKI format.");
});

// Export:
var rsaKeyGenParams = {
    name: "RSA-OAEP",
    modulusLength: 2048,
    publicExponent: new Uint8Array([0x01, 0x00, 0x01]),  // Equivalent to 65537
    hash: "sha-256"
};
crypto.subtle.generateKey(rsaKeyGenParams, true, ["decrypt", "encrypt"]).then(function(keyPair) {
    crypto.subtle.exportKey("spki", keyPair.publicKey).then(function(binary) {
        console.log("A RSA-OAEP key is exported via SPKI format.");
    });
});

生成公钥证书的第三方实时示例可以在这里找到。

3. 异步执行耗时的 SubtleCrypto 方法

在之前的 WebKitSubtleCrypto 实现中,只有 RSA 的 generateKey 是异步执行的,而所有其他操作都是同步的。尽管同步操作对于快速完成的方法效果很好,但大多数加密方法都很耗时。因此,新 SubtleCrypto 实现中的所有耗时方法都异步执行

方法 encrypt decrypt sign verify digest generateKey* deriveKey deriveBits importKey exportKey wrapKey* unwrapKey*
异步

请注意,只有 RSA 密钥对生成是异步的,而 EC 密钥对和对称密钥生成是同步的。还要注意,AES-KW 是唯一的例外,对于 wrapKey/unwrapKey 仍然执行同步操作。通常密钥大小只有几百字节,因此加密/解密如此少量的数据耗时较少。AES-KW 是唯一直接支持 wrapKey/unwrapKey 操作的算法,而其他算法则桥接到 encrypt/decrypt 操作。因此,它成为唯一执行 wrapKey/unwrapKey 同步操作的算法。Web 开发者可以将每个 SubtleCrypto 函数视为任何其他返回 promise 的函数。

4. Web Worker 支持

除了使大多数 API 异步化之外,我们还支持 web worker,以允许另一种异步执行模型。开发者可以选择最适合其需求的方式。结合这两种模型,开发者现在可以将加密原语集成到他们的网站中,而不会阻塞任何 UI 活动。web worker 中的 SubtleCrypto 对象使用与 Window 对象中的对象相同的语义。以下是一些使用 web worker 加密文本的代码示例

// In Window. 
var rawKey = asciiToUint8Array("16 bytes of key!");
crypto.subtle.importKey("raw", rawKey, {name: "aes-cbc", length: 128}, true, ["encrypt", "decrypt"]).then(function(localKey) {
    var worker = new Worker("crypto-worker.js");
    worker.onmessage = function(evt) {
        console.log("Received encrypted data.");
    }
    worker.postMessage(localKey);
});

// In crypto-worker.js.
var plainText = asciiToUint8Array("Hello, World!");
var aesCbcParams = {
    name: "aes-cbc",
    iv: asciiToUint8Array("jnOw99oOZFLIEPMr"),
}
onmessage = function(key)
{
    crypto.subtle.encrypt(aesCbcParams, key, plainText).then(function(cipherText) {
        postMessage(cipherText);
    });
}

一个实时示例位于此处,演示了异步执行如何帮助创建更具响应性的网站。

除了上述四个主要改进领域之外,一些值得一提的次要变化包括

  • CryptoKey 接口增强,包括从 Key 重命名为 CryptoKey,使 algorithm 和 usages 槽位可缓存,并将其暴露给 web worker。
  • HmacKeyParams.length 现在以位(bits)为单位,而不是字节(bytes)。
  • RSA-OAEP 现在可以使用 SHA-256 导入和导出密钥。
  • CryptoKeyPair 现在是字典类型。

新增加密算法

除了新的 SubtleCrypto 接口,本次更新还增加了对许多加密算法的支持

  1. AES-CFB:CFB 代表密文反馈(cipher feedback)。与 CBC 不同,CFB 不需要明文填充到块大小。
  2. AES-CTR:CTR 代表计数器模式(counter mode)。CTR 因其在加密和解密方面的并行化能力而闻名。
  3. AES-GCM:GCM 代表伽罗瓦/计数器模式(Galois/Counter Mode)。GCM 是一种认证加密算法,旨在提供数据真实性(完整性)和机密性。
  4. ECDH:ECDH 代表椭圆曲线 Diffie–Hellman。椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线代数结构的公钥密码学方法。与 RSA 相比,ECC 需要更小的密钥来提供同等的安全性。ECDH 是众多 ECC 方案之一。它允许拥有 ECC 密钥对的双方通过不安全通道建立共享密钥。
  5. ECDSA:ECDSA 代表椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm)。这是另一种 ECC 方案。
  6. HKDF:HKDF 代表基于 HMAC 的密钥派生函数(HMAC-based Key Derivation Function)。它将秘密转换为密钥,允许在需要时组合额外的非秘密输入。
  7. PBKDF2:PBKDF2 代表基于密码的密钥派生函数 2(Password-Based Key Derivation Function 2)。它接受密码或口令以及盐值来派生加密对称密钥。
  8. RSA-PSS:PSS 代表概率签名方案(Probabilistic Signature Scheme)。它是 RSA 的一种改进的数字签名算法。

这组新算法不仅增加了新的功能(例如密钥派生函数),还通过替换具有相同功能的现有算法,为开发者带来了更高的效率和更好的安全性。为了演示这些优势,以下展示了使用选定新算法编写的示例代码片段。这些示例中的实现并非最佳实践,仅用于演示目的。

示例 1: AES-GCM

以前,AES-CBC 是唯一可用的块密码,用于加密/解密。尽管它在保护数据机密性方面表现出色,但它不保护生成的密文的真实性(完整性)。因此,它通常与 HMAC-SHA256 捆绑使用,以防止密文被静默破坏。以下是相应的代码片段

// Assume aesKey and hmacKey are imported before with the same raw key.
var plainText = asciiToUint8Array("Hello, World!");
var aesCbcParams = {
    name: "aes-cbc",
    iv: asciiToUint8Array("jnOw99oOZFLIEPMr"),
}

// Encryption:
// First encrypt the plain text with AES-CBC.
crypto.subtle.encrypt(aesCbcParams, aesKey, plainText).then(function(result) {
    console.log("Plain text is encrypted.");
    cipherText = result;

    // Then sign the cipher text with HMAC.
    return crypto.subtle.sign("hmac", hmacKey, cipherText);
}).then(function(result) {
    console.log("Cipher text is signed.");
    signature = result;

    // Finally produce the final result by concatenating cipher text and signature.       
    finalResult = new Uint8Array(cipherText.byteLength + signature.byteLength);
    finalResult.set(new Uint8Array(cipherText));
    finalResult.set(new Uint8Array(signature), cipherText.byteLength);
    console.log("Final result is produced.");
});

// Decryption:
// First decode the final result from the encryption step.
var position = finalResult.length - 32; // SHA-256 length
signature = finalResult.slice(position);
cipherText = finalResult.slice(0, position);

// Then verify the cipher text.
crypto.subtle.verify("hmac", hmacKey, signature, cipherText).then(function(result) {
    if (result) {
        console.log("Cipher text is verified.");

        // Finally decrypt the cipher text.
        return crypto.subtle.decrypt(aesCbcParams, aesKey, cipherText);
    } else
        return Promise.reject();
}).then(function(result) {
    console.log("Cipher text is decrypted.");
    decryptedText = result;
}, function() {
    // Error handling codes ...
});

到目前为止,使用 AES-CBC 的代码由于 HMAC 的额外开销而有些复杂。然而,使用 AES-GCM 实现相同的认证加密效果要简单得多,因为它将认证和加密捆绑在一个步骤中。以下是相应的代码片段

// Assume aesKey are imported/generated before, and the same plain text is used.
var aesGcmParams = {
    name: "aes-gcm",
    iv: asciiToUint8Array("jnOw99oOZFLIEPMr"),
}

// Encryption:
crypto.subtle.encrypt(aesGcmParams, key, plainText).then(function(result) {
    console.log("Plain text is encrypted.");
    cipherText = result; // It contains both the cipherText and the authentication data.
});

// Decryption:
crypto.subtle.decrypt(aesGcmParams, key, cipherText).then(function(result) {
    console.log("Cipher text is decrypted.");
    decryptedText = result;
}, function(error) {
    // If any violation of the cipher text is detected, the operation will be rejected.
    // Error handling codes ...
});

使用 AES-GCM 就是这么简单。这种简单性必将提高开发者的效率。一个实时示例可以在此处找到,演示 AES-GCM 如何在解密损坏的密文时防止静默损坏。

示例 2: ECDH(E)

仅有块密码不足以保护数据机密性,因为 secret(对称)密钥也需要安全共享。在此更改之前,只有 RSA 加密可用于处理此任务。即加密共享 secret 密钥,然后交换密文以防止 MITM 攻击。此方法并非完全安全,因为完美前向保密(PFS)难以保证。PFS 要求会话密钥(在本例中为 RSA 密钥对)在会话完成后立即销毁,即成功共享 secret 密钥后。因此,即使 MITM 攻击者能够记录交换的密文并在将来访问接收者,也永远无法恢复共享 secret 密钥。RSA 密钥对生成非常困难,因此维护 RSA secret 密钥交换的 PFS 确实是一个挑战。

然而,对于 ECDH 来说,维护 PFS 是小菜一碟,原因很简单,EC 密钥对很容易生成。在第一节所示的相同测试环境下,生成 RSA-2048 密钥对平均需要约 170 毫秒。相反,生成可提供与 RSA-3072 替代方案相当的安全性的 P-256 EC 密钥对仅需约 2 毫秒。ECDH 的工作方式是,参与的双方首先交换它们的公钥,然后使用获取的公钥和它们各自的私钥计算点乘,其结果就是共享密钥。带有 PFS 的 ECDH 被称为 Ephemeral ECDH (ECDHE)。Ephemeral 仅意味着此协议中的会话密钥是临时的。由于 ECDH 所涉及的 EC 密钥对是临时的,它们不能用于确认参与双方的身份。因此,需要其他永久的非对称密钥对来进行身份验证。通常使用 RSA,因为它被常见的公钥基础设施(PKI)广泛支持。为了演示 ECDHE 的工作原理,以下代码片段进行了分享

// Assuming Bob and Alice are the two parties. Here we only show codes for Bob's.
// Alice's should be similar.
// Also assumes that permanent RSA keys are obtained before, i.e. bobRsaPrivateKey and aliceRsaPublicKey.
// Prepare to send the hello message which includes Bob's public EC key and its signature to Alice:
// Step 1: Generate a transient EC key pair.
crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, extractable, ["deriveKey"]).then(function(result) {
    console.log("EC key pair is generated.");
    bobEcKeyPair = result;

    // Step 2: Sign the EC public key for authentication.
    return crypto.subtle.exportKey("raw", bobEcKeyPair.publicKey);
}).then(function(result) {
    console.log("EC public key is exported.");
    rawEcPublicKey = result;

    return crypto.subtle.sign({ name: "RSA-PSS", saltLength: 16 }, bobRsaPrivateKey, rawEcPublicKey);
}).then(function(result) {
    console.log("Raw EC public key is signed.");
    signature = result;

    // Step 3: Exchange the EC public key together with the signature. We simplify the final result as
    // a concatenation of the raw format EC public key and its signature.
    finalResult = new Uint8Array(rawEcPublicKey.byteLength + signature.byteLength);
    finalResult.set(new Uint8Array(rawEcPublicKey));
    finalResult.set(new Uint8Array(signature), rawEcPublicKey.byteLength);
    console.log("Final result is produced.");

    // Send the message to Alice.
    // ...
});

// After receiving Alice's hello message:
// Step 1: Decode the counterpart from Alice.
var position = finalResult.length - 256; // RSA-2048
signature = finalResult.slice(position);
rawEcPublicKey = finalResult.slice(0, position);

// Step 2: Verify Alice's signature and her EC public key.
crypto.subtle.verify({ name: "RSA-PSS", saltLength: 16 }, aliceRsaPublicKey, signature, rawEcPublicKey).then(function(result) {
    if (result) {
        console.log("Alice's public key is verified.");

        return crypto.subtle.importKey("raw", rawEcPublicKey, { name: "ECDH", namedCurve: "P-256" }, extractable, [ ]);
    } else
        return Promise.reject();
}).then(function(result) {
    console.log("Alice's public key is imported.");
    aliceEcPublicKey = result;

    // Step 3: Compute the shared AES-GCM secret key.
    return crypto.subtle.deriveKey({ name: "ECDH", public: aliceEcPublicKey }, bobEcKeyPair.privateKey, { name: "aes-gcm", length: 128 }, extractable, ['decrypt', 'encrypt']);
}).then(function(result) {
    console.log("Shared AES secret key is computed.");
    aesKey = result;

    console.log(aesKey);

    // Step 4: Delete the transient EC key pair.
    bobEcKeyPair = null;
    console.log("EC key pair is deleted.");
});

在上面的示例中,我们省略了信息(即公钥及其相应参数)的交换方式,以专注于 WebCrypto API 所涉及的部分。轻松实现 ECDHE 必将提高密钥交换的安全级别。此外,此处还包含一个实时示例,用于说明 RSA secret 密钥交换和 ECDH 之间的差异。

示例 3: PBKDF2

从现有秘密(例如密码)派生密码学 secret 密钥的能力是新增的。PBKDF2 是新增算法之一,可以实现此目的。从 PBKDF2 派生的 secret 密钥不仅可以在后续的密码学操作中使用,而且它本身也是一个强大的加盐密码哈希。以下代码片段演示了如何从简单密码派生强大的密码哈希

var password = asciiToUint8Array("123456789");
var salt = asciiToUint8Array("jnOw99oOZFLIEPMr");

crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits"]).then(function(baseKey) {
    return crypto.subtle.deriveBits({name: "PBKDF2", salt: salt, iterations: 100000, hash: "sha-256"}, baseKey, 128);
}).then(function(result) {
    console.log("Hash is derived!")
    derivedHash = result;
});

一个实时示例可以在此处找到。

上述示例仅是 WebCrypto API 功能的冰山一角。以下表格列出了 WebKit 当前支持的所有算法以及每种算法允许的操作。

算法名称 encrypt decrypt sign verify digest generateKey deriveKey deriveBits importKey** exportKey** wrapKey unwrapKey
RSAES-PKCS1-v1_5***
RSASSA-PKCS1-v1_5
RSA-PSS
RSA-OAEP
ECDSA*
ECDH*
AES-CFB
AES-CTR
AES-CBC
AES-GCM
AES-KW
HMAC
SHA-1***
SHA-224
SHA-256
SHA-384
SHA-512
HKDF
PBKDF2
* WebKit 尚不支持 P-521,请参阅bug 169231
** WebKit 不检查或生成来自或去往 DER 密钥数据的任何哈希信息,请参阅bug 165436bug 165437
*** 出于安全考虑,应避免使用 RSAES-PKCS1-v1_5 和 SHA-1。

过渡到新的 SubtleCrypto 接口

本节介绍了一些 Web 开发者在尝试维护 WebKitSubtleCryptoSubtleCrypto 兼容性时常犯的错误,然后提供了针对这些错误的建议修复方法。最后,我们将这些修复方法总结为一条事实上的兼容性维护规则。

示例 1

// Bad code:
var subtleObject = null;
if ("subtle" in self.crypto)
    subtleObject = self.crypto.subtle;
if ("webkitSubtle" in self.crypto)
    subtleObject = self.crypto.webkitSubtle;

此示例错误地将 window.crypto.webkitSubtle 的优先级置于 window.crypto.subtle 之上。因此,即使 subtle 对象实际存在,它也会覆盖 subtleObject。快速修复方法是将 window.crypto.subtle 的优先级置于 window.crypto.webkitSubtle 之上。

// Fix:
var subtleObject = null;
if ("webkitSubtle" in self.crypto)
    subtleObject = self.crypto.webkitSubtle;
if ("subtle" in self.crypto)
    subtleObject = self.crypto.subtle;

示例 2

// Bad code:
(window.agcrypto = window.crypto) && !window.crypto.subtle && window.crypto.webkitSubtle && (console.log("Using crypto.webkitSubtle"), window.agcrypto.subtle = window.crypto.webkitSubtle);
var h = window.crypto.webkitSubtle ? a.utils.json2ab(c.jwkKey) : c.jwkKey;
agcrypto.subtle.importKey("jwk", h, g, !0, ["encrypt"]).then(function(a) {
    ...
});

此示例错误地将 window.agcrypto 与后面的 jwkKey 配对。第一行将 window.crypto.subtle 的优先级置于 window.crypto.webkitSubtle 之上,这是正确的。然而,第二行再次将 window.crypto.webkitSubtle 的优先级置于 window.crypto.subtle 之上。

// Fix:
(window.agcrypto = window.crypto) && !window.crypto.subtle && window.crypto.webkitSubtle && (console.log("Using crypto.webkitSubtle"), window.agcrypto.subtle = window.crypto.webkitSubtle);
var h = window.crypto.subtle ? c.jwkKey : a.utils.json2ab(c.jwkKey);
agcrypto.subtle.importKey("jwk", h, g, !0, ["encrypt"]).then(function(a) {
    ...
});

对这些示例的深入分析表明,它们都假定 window.crypto.subtlewindow.crypto.webkitSubtle 不能共存,因此错误地将其中一个优先于另一个。总之,开发者应该意识到这两个接口的共存,并且应该始终将 window.crypto.subtle 的优先级置于 window.crypto.webkitSubtle 之上。

反馈

在这篇博客文章中,我们回顾了 WebKit 对 WebCrypto API 实现的更新,该更新已在 macOS、iOS 和 GTK+ 上可用。希望您喜欢。您可以在最新的 Safari Technology Preview 中尝试所有这些改进。通过 Twitter(@webkit@alanwaketan@jonathandavis)发送反馈或提交 Bug 让我们了解它们的使用情况。