本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988… 本文阅读指数:3
本文涉及到了很多加密算法,原文说的也很简单,不是很好理解。需要大家看完之后,再找一些辅助材料阅读一下。
密码学是IT界的重要领域。无数人在网络上互相交流,在信息传递的过程中就可能有人窃取或者劫持,可能有人利用网络弱点窃取个人信息。
人们如何安全发送信息,以及JS扮演了什么角色?如果你不知道答案,那么这一章就很适合你。 在这一章,我们将会知道什么是密码学,以及在JS中如何工作,以及如何处理中间人攻击。
什么是加密
加密是处理安全信息和交流的过程,这样只有发送者和目标接受者才能访问它们。加密有多种技术方式。这些即使包含加密和解密,使用不同的算法来哈希通信过程,签名和验证。
由于很多人使用JS构建的应用来进行通信的,所以有必要理解JS是怎么加密的。下一节。我们会看看JSWEB的加密API以及如何加密的。
JS Web 的加密API
由于确保网络通信是很重要的,一些web浏览器已经实现了crypto接口。
但是这个接口不能很好的定义或者加密声音信息。JS web 加密API提供了一个更好的接口
SubtleCryptoSubtleCrypto
.
JS web加密API允许开发者在应用中使用基本的加密方法而不需要第三方的库。你可以签名文档,执行认证,对整个通信进行校验。
比如,你可以这样获得一个加密的数组:
const secure = window.crypto.getRandomValues(new Uint8Array(10));
console.log(secure);
在控制台执行这段代码,能看到10个随机生成的数字
下面就看看加密API是如何工作的,以及我们的控制台是怎么做到的。
合理使用加密API,服务器就无法看到数据,只有发送者和接受者可以访问到。
从上图中可以看到,发送者使用了API进行加密,接受者对数据进行解密。而服务器和数据库不能加密也不能和解密。 你可以执行很多加密操作,比哈希,签名,认证等等,我们后面会讨论
基础的加密方式
Encrypting
JS的API encrypt
方法就是这样的:
//Syntax for encrypt function
const result = crypto.subtle.encrypt(algorithm, key, data);
这个加密方法会返回一个Promise
,内部是包含密电码的ArrayBuffer
。在加密时如果发生了异常,就会返回一个新的Promise rejected,内部是一个 序列化算法的结果。
举个例子,我们加密一个纯文本,使用AES-GCM
密钥和算法。把代码复制到你的控制台,看一下输出的密电码。
/*The function strToArrayBuffer converts string to fixed-length raw binary data buffer because
encrypt method must return a Promise that fulfills with an ArrayBuffer containing the "ciphertext"*/
function strToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length * 2);
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
//The function arrayBufferToString converts fixed-length raw binary data buffer to 16-bit unsigned String as our plaintext
function arrayBufferToString(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
//This object below will generate our algorithm key
var algoKeyGen = {
name: "AES-GCM",
length: 256,
};
//This will generate random values of 8-bit unsigned integer
var iv = window.crypto.getRandomValues(new Uint8Array(12));
//This object will generate our encryption algorithm
var algoEncrypt = {
name: "AES-GCM",
iv: iv,
tagLength: 128,
};
//states that key usage is for encryption
var keyUsages = ["encrypt"];
var plainText = "This is a secure message from Mary";
var secretKey;
//This generates our secret Key with key generation algorithm
window.crypto.subtle
.generateKey(algoKeyGen, false, keyUsages)
.then(function (key) {
secretKey = key;
//Encrypt plaintext with key and algorithm converting the plaintext to ArrayBuffer
return window.crypto.subtle.encrypt(
algoEncrypt,
key,
strToArrayBuffer(plainText)
);
})
.then(function (cipherText) {
//print out Ciphertext in console
console.log("Cipher Text: " + arrayBufferToString(cipherText));
})
.catch(function (err) {
console.log("Error: " + err.message);
});
看代码
var algoKeyGen = { name: ‘AES-GCM’, length: 256};
这里声明了密钥
var algoEncrypt = { name: ‘AES-GCM’, iv: iv, tagLength: 128};
这里声明了加密算法
Decrypting
Decrypting是跟加密相反,把密电码转换为纯文本。这时候就需要认证用户提供一个密钥。
decrypt
方法可以来解密,它的语法如下:
const result = crypto.subtle.decrypt(algorithm, key, data);
上面例子的解密过程如下:
//This states that the keyusage for decrypting
var keyUsages = ["decrypt"];
//This object below is for algorithm key generation
var algoKeyGen = {
name: "AES-GCM",
length: 256,
};
var plainText = "This is a secure message from Mary";
var secretKey;
//This will generate secrete key with algorithm key and keyusage
window.crypto.subtle
.generateKey(algoKeyGen, false, keyUsages)
.then(function (key) {
secretKey = key;
//This will decrypt Cipheretext to plaintext
return window.crypto.subtle.decrypt(algoEncrypt, secretKey, cipherText);
})
// Print plaintext in console.
.then(function (plainText) {
console.log("Plain Text: " + arrayBufferToString(plainText));
})
.catch(function (err) {
console.log("Error: " + err.message);
});
完整的加密解密代码如下:
// This code below will encrypt and decrypt plaintext
/*The function strToArrayBuffer converts string to fixed-length raw binary data buffer because
encrypt method must return a Promise that fulfills with an ArrayBuffer containing the "ciphertext"*/
function strToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length * 2);
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
//The function arrayBufferToString converts fixed-length raw binary data buffer to 16-bit unsigned String as our plaintext
function arrayBufferToString(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
//This object below will generate our algorithm key
var algoKeyGen = {
name: "AES-GCM",
length: 256,
};
//This will generate random values of 8-bit unsigned integer
var iv = window.crypto.getRandomValues(new Uint8Array(12));
//This object will generate our encryption algorithm
var algoEncrypt = {
name: "AES-GCM",
iv: iv,
tagLength: 128,
};
//states that key usage is for encrypting and decrypting
var keyUsages = ["encrypt", "decrypt"];
var plainText = "This is a secure message from Mary";
var secretKey;
//This generates our secret Key with key generation algorithm
window.crypto.subtle
.generateKey(algoKeyGen, false, keyUsages)
.then(function (key) {
secretKey = key;
//Encrypt plaintext with key and algorithm converting the plaintext to ArrayBuffer
return window.crypto.subtle.encrypt(
algoEncrypt,
key,
strToArrayBuffer(plainText)
);
})
.then(function (cipherText) {
//This prints out the ciphertext, converting it from ArrayBuffer to 16-bit unsigned String
console.log("Cipher Text: " + arrayBufferToString(cipherText));
//This will decrypt ciphertext with secret key and algorithm
return window.crypto.subtle.decrypt(algoEncrypt, secretKey, cipherText);
})
//This prints out the plaintext, converting it from ArrayBuffer to 16-bit unsigned String
.then(function (plainText) {
console.log("Plain Text: " + arrayBufferToString(plainText));
})
.catch(function (err) {
console.log("Error: " + err.message);
});
运行这段代码,将会看到之前加密的纯文本
看代码
var secretKey;
window.crypto.subtle.generateKey(algoKeyGen, false, keyUsages)
.then(function (key) { secretKey = key;
这里生成可以密钥来加密信息
加密过程可以宽泛的分为对称和非对称加密。这取决于加密使用的密钥类型。对称机密的话,加密和解密使用相同的密钥。非对称则相反,加密和解密采用不同的密钥。加密使用认证用户之间共享的公钥,接受者使用私钥来解密。
Hashing
哈希是一个加密方法,可以把任意长度的数据映射成固定尺寸的数组。加密哈希函数,把纯文本数据转换成一个由数组和字母组成的唯一的字符串。哈希不同于上面说的加密,但是单向的。也就是说,你就可以加密,但是基本不可能把哈希后的数据转换为源文本
哈希只能加密不能解析,所以主要用来进行认证。比如,注册或登录。当用户注册是,密码在存进数据库之前先哈希。当用户登录时,登录密码被哈希,然后跟数据库中的数据再对比确保匹配。这样,即使黑客攻入了数据库,获得了用户的账户信息,它还是无法解码出源密码。
JS可以调用crypto.subtle.digest
来进行哈希。你可以使用SHA-1
, SHA-384
和SHA-512
几种方式来哈希:
const digest = crypto.subtle.digest(algorithm, data);
看一个例子:
const text = "This is a secure message from Mary";
async function digestMessage(message) {
const encode = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest("SHA-256", encode); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
const digestHex = await digestMessage(text);
console.log(digestHex);
在这个例子中,我们使用了SHA-256
算法来加密文本This is a secure message from Mary ,将其转换为十六进制的字符串。在生产环境中,不推荐使用SHA-1
算法,因为它比较弱。
生成签名和验证
这是另一种加密方式。使用sign
和 verify
函数,你可以用一个密钥给文档加密。接受者使用他们的密钥去验证文档。
签名和验证的语法如下:
//syntax to sign document
const signature = crypto.subtle.sign(algorithm, key, data);
//syntax to generate document
const result = crypto.subtle.verify(algorithm, key, signature, data);
加密算法
加密有很多种不同的SHA算法,每一个都声明了哈希值的bit位长度 。这一节,我们会看看JS支持的加密函数。
ECDH (Elliptic Curve Diffie-Hellman)
这个加密算法,用来生成和协商密钥。ECDH密钥协商协议,允许通信双方持有一个私有-公有的密钥对来保护通信。
这个算法允许你做这些事情
- 生成密钥
- Bits Derivation
- 导入密钥
- 到处密钥
SHA (Secure Hash Algorithm)
这个算法大多是哈希的时候使用。它将任意数据压缩成定长的比特字符串。JS API允许你使用crypto.subtle.digest ,它支持 SHA-1
和 SHA-2
,也支持"SHA-256”, “SHA-384”, “SHA-512”
HMAC (Hash-based Message Authentication Code)
使用哈希函数哈希之后,需要验证输入的信息是否跟这个哈希值相同。记住,我们无法把哈希值转换成源文本。为了对比值,HMAC
算法可以用来签名和认证文档。你可以用这个算法做这个:
- 签名
- 验证
- 生成密钥
- 导入密钥
- 导出密钥
- 获取密钥长度
HKDF (Hash-based Key Derivation Function)
HKDF是一个基于HMAC的加密派生方法。它使用了extraction-then-expansion(先提取后扩充) 方式来实现。使用这个算法,可以把公钥转换成密钥,用来加密,验证或者认证。你可以用这个算法做这些事情:
- Bits Derivation
- 导入密钥
- 获取密钥长度
ECDSA (Elliptic Curve Digital Signature Algorithm)
ECDSA允许用户签名和认证文档,使用 elliptic curve cryptography。你可以用这个算法做这些事情:
- 签名
- 验证
- 生成密钥
- 导入密钥
- 导出密钥
RSA (Rivest–Shamir–Adleman) Algorithm
RSA用来加密网络信息,它是一个异步的算法。加密和解密使用了两个密钥。一个是公钥,在认证用户之间共享,另一个是私钥,必须私人保护。
RSA还支持其他的 padding schemes算法。JS web加密API支持下面这些RSA算法
- RSASSA-PKCS1-v1_5
- RSA-PSS
- RSA-OAEP
AES (Advanced Encryption Standard) Algorithm
The advanced encryption standard algorithm is mostly known by its original name Rijndael. It is used for the encryption of electronic data and was established by the U.S. National Institute of Standards and Technology (NIST) in 2001. JavaScript’s Web Cryptography API supports different encryption models that use the AES block, they are:
- AES-CTR
- AES-CBC
- AES-GCM
- AES-KW
JSweb 加密API的使用场景
很多JS应用需要实时加密。在这一节,我们看看实时加密的各类应用。
多元素认证
即使用户的密码被哈希或者被加密了,但是黑客们还是窃取用户的账户。为了确保登录账户是真正的使用者,很多应用要做多元素认证。
而不是使用传输层的认证,比如TLS客户端认证应用使用灵活的用户密钥,这些密钥是使用用户代理预先生成的。
受保护的文档交换
加密的一个目标就是在第三方面前,能够确保信息的安全。JS 加密API允许你给文档签名,验证和加密。只允许拥有密钥的认证用户有权限访问文档。
云存储
使用JS加密API可以在上传文档之前就保护他们。应用使用用户选择的密钥,派生出一个加密的密钥,把文档加密了。然后再上传到服务器
安全消息
用户可以在网上使用OTR(无痕)的方式通信。通信双发可以使用验证码密钥(MAC)加密和解密信息,来保护数据。
JS对象签名和加密(JOSE)
可以使用JS加密API来给JOSE的结构和信息进行交互。
怎么处理MitM攻击
MITM可以被称作中间攻击中的机器,也可是指中间攻击中的人。它是一种网络攻击,监听通信的双方,去修改他们的通信内容。
例如,中间人可以在发送者的信息达到接受者之前就劫持信息。假如Nancy和Joy有一个加密会话。
这时一个中间人James,获得了Nacy的密钥,他就可以解密信息,然后再发送给Joy之前进行篡改。
一般怎么处理MITM攻击呢?
- 使用篡改检测是检验是否发生了MITM攻击的有效方式,通信双方可以对比响应时间的差异。
- 双向认证可以有效减少MITM攻击,服务端和客户端互相验证
- 取证分析同样很重要,它可以显示是否发生了MIIT攻击,以及攻击的资源。
安全是个大话题
人人都希望自己在网上的通信交流是安全的,不会被泄漏出去。 开发者必须小心的选择第三方的工具和合作的供应商。要确保使用的工具在安全性和隐私性上都已经被相关的组织认证了。