Web 加密 API 实用指南
客户端加密是我一直以来想在Octo中实现的一个功能。当我终于要开始着手实现它时,却惊讶地发现关于这个主题的实际示例竟然如此稀少。MDN 上的文档很丰富,但实现它需要反复跳转到各个方法的 API。希望这篇文章能对那些寻求指导的人有所帮助。
注意:Web Cryptography API 是异步的,因此为了简洁起见,我在本文中使用了 async/await 语法。
SubtleCrypto
Web 加密 API 最初通过名为Crypto的非标准接口公开,但后来通过名为SubtleCrypto的新接口实现了标准化。本文将重点介绍公开于 的 SubtleCrypto 接口window.crypto.subtle
。
加密
就本文而言,我们将使用对称算法。公钥(非对称)策略根据密钥大小对可加密的数据量有严格的限制: 。对称加密使用相同的密钥来加密和解密数据,因此没有相同的限制。目前支持的算法(keyBits / 8) - padding
有几种,但推荐的对称算法适用于其认证模式。AES-GCM
生成密钥
首先,我们需要生成一个对称密钥。
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
const generateKey = async () => {
return window.crypto.subtle.generateKey({
name: 'AES-GCM',
length: 256,
}, true, ['encrypt', 'decrypt'])
}
编码数据
在加密数据之前,我们必须先将其编码成字节流。我们可以用这个类轻松实现这一点TextEncoder
。这个小工具稍后会在函数中使用encrypt
。
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
const encode = (data) => {
const encoder = new TextEncoder()
return encoder.encode(data)
}
生成初始化向量(IV)
简而言之,IV 为我们的加密策略引入了真正的随机性。当使用相同的密钥加密多组数据时,可以推导出加密数据块之间的关系,从而泄露部分或全部原始消息。IV 确保输入数据中重复的字符序列在生成的密码中产生不同的字节序列。将 IV 与加密消息一起以纯文本形式存储是完全安全的,稍后我们需要这样做来解密消息。
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const generateIv = () => {
// https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams
return window.crypto.getRandomValues(new Uint8Array(12))
}
我们从不想对给定的密钥使用相同的 IV,因此最好将自动 IV 生成纳入我们的加密策略中,就像我们稍后会做的那样。
加密数据
现在我们已经准备好了所有实用程序,可以实现我们的encrypt
功能了!如上所述,我们需要它返回密码和初始向量 (IV),以便稍后解密密码。
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt
const encrypt = async (data, key) => {
const encoded = encode(data)
const iv = generateIv()
const cipher = await window.crypto.subtle.encrypt({
name: 'AES-GCM',
iv: iv,
}, key, encoded)
return {
cipher,
iv,
}
}
传输和存储
大多数实际的加密应用都涉及加密数据的传输或存储。使用 SubtleCrypto 加密数据时,生成的密码和初始向量 (IV) 会以原始二进制数据缓冲区的形式表示。这并非理想的传输或存储格式,因此我们接下来将讨论打包和解包。
包装数据
由于数据通常以 JSON 格式传输并存储在数据库中,因此将数据打包成可移植的格式是有意义的。我们将把二进制数据缓冲区转换为 base64 编码的字符串。根据您的用例,base64 编码完全是可选的,但我发现它有助于提高数据的可移植性,满足您的需求。
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const pack = (buffer) => {
return window.btoa(
String.fromCharCode.apply(null, new Uint8Array(buffer))
)
}
解包数据
一旦我们的打包数据被传输、存储并随后检索,我们只需逆转该过程即可。我们将把 base64 编码的字符串转换回原始二进制缓冲区。
// https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
const unpack = (packed) => {
const string = window.atob(packed)
const buffer = new ArrayBuffer(string.length)
const bufferView = new Uint8Array(buffer)
for (let i = 0; i < string.length; i++) {
bufferView[i] = string.charCodeAt(i)
}
return buffer
}
解密
我们已经到了最后冲刺阶段!这个过程的最后一步是解密数据,看看那些甜蜜的秘密。和解包一样,我们只需要逆向加密过程即可。
解码数据
解密后,我们需要将得到的字节流解码回其原始形式。我们可以用TextDecoder
类来实现这一点。这个实用程序稍后会被我们的函数使用decrypt
。
// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
const decode = (bytestream) => {
const decoder = new TextDecoder()
return decoder.decode(bytestream)
}
解密数据
现在我们只需要实现该decrypt
函数。如前所述,我们不仅需要提供密钥,还需要提供加密步骤中使用的IV。
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt
const decrypt = async (cipher, key, iv) => {
const encoded = await window.crypto.subtle.decrypt({
name: 'AES-GCM',
iv: iv,
}, key, cipher)
return decode(encoded)
}
付诸实践
让我们编写一个应用程序!现在所有实用程序都已构建完毕,我们只需使用它们。我们将加密、打包并将数据传输到安全端点。然后,我们将检索、解包并解密原始消息。
const app = async () => {
// encrypt message
const first = 'Hello, World!'
const key = await generateKey()
const { cipher, iv } = await encrypt(first, key)
// pack and transmit
await fetch('/secure-api', {
method: 'POST',
body: JSON.stringify({
cipher: pack(cipher),
iv: pack(iv),
}),
})
// retrieve
const response = await fetch('/secure-api').then(res => res.json())
// unpack and decrypt message
const final = await decrypt(unpack(response.cipher), key, unpack(response.iv))
console.log(final) // logs 'Hello, World!'
}
就这样!我们成功实现了客户端加密。
最后,我想再次分享octo,一款面向开发者的写作应用。它是免费的,开源的,如果你试用一下,我会非常高兴。谢谢大家,祝你编程愉快!✌️
文章来源:https://dev.to/voracious/a-practical-guide-to-the-web-cryptography-api-4o8n