一、密码学里面的相关概念
-
加密、解密 加密,是以某种特殊的算法改变原有的信息数据,使得未授权的用户即使获得了已加密的信息,但因不知解密的方法,仍然无法了解信息的内容。未加密之前的信息我们叫做明文,明文经过加密后的信息叫做密文。
反过来,用特殊的算法将拿到密文给转化成明文,这个过程就叫解密。 -
密钥 密钥是一种参数,它是在明文转换为密文或将密文转换为明文的算法中输入的参数。密钥分为对称密钥与非对称密钥。
对称密钥加密,又称私钥加密或会话密钥加密算法,即信息的发送方和接收方使用同一个密钥去加密和解密数据。它的最大优势是加/解密速度快,适合于对大数据量进行加密,但密钥管理困难。
非对称密钥加密系统,又称公钥密钥加密。它需要使用不同的密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。公钥机制灵活,但加密和解密速度却比对称密钥加密慢得多。
二、常用的几种简单加密算法
- md5
MD5算法的原理可简要的叙述为:MD5码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。
JavaScript中使用md5非常简单,需要注意的是,MD5加密是不可逆的,无法将加密后的密文转成明文。md5通常用于将用户密码转成密文,后续验证无须转成明文,只需要将用户输入的密码用md5加密后对比即可。
npm install js-md5
md5('123456');
- sha1 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,美国国家安全局设计,并由美国国家标准技术研究所(NIST)发布为联邦数据处理标准(FIPS)。SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。
SHA-1已经不再视为可抵御有充足资金、充足计算资源的攻击者。2005年,密码分析人员发现了对SHA-1的有效攻击方法,这表明该算法可能不够安全,不能继续使用,自2010年以来,许多组织建议用SHA-2或SHA-3来替换SHA-1。Microsoft、Google以及Mozilla都宣布,它们旗下的浏览器将在2017年前停止接受使用SHA-1算法签名的SSL证书。
npm install js-sha1
sha1('123456');
md5 和 sha1通常用来验证数据是否被修改或数据是否一致,因为其计算速度较快,而当文本或文件内容有所修改时,md5 和 sha1将会发生非常大的变化,所以常常被用来验证字符串或文件是否被串改。
三、Web Cryptography API
Web Cryptography API描述了一套密码学工具,规范了JavaScript如何以安全和符合惯例的方式实现加密。这些工具包括生成、使用和应用加密密钥对,加密和解密消息,以及可靠地生成随机数。
注意 加密接口的组织方式有点奇怪,其外部是一个Crypto对象,内部是一个SubtleCrypto对象。在Web Cryptography API标准化之前,window.crypto属性在不同浏览器中的实现差异非常大。为实现跨浏览器兼容,标准API都暴露在SubtleCrypto对象上。
1.生成随机数
在需要生成随机值时,很多人会使用Math.random()。这个方法在浏览器中是以伪随机数生成器(PRNG,PseudoRandom Number Generator)方式实现的。所谓“伪”指的是生成值的过程不是真的随机。在某种情况下,Math.random()产生的值其实是可以被计算出来的。
Web Cryptography API引入了CSPRNG,这个CSPRNG可以通过crypto.getRandomValues()在全局Crypto对象上访问。与Math.random()返回一个介于0和1之间的浮点数不同,getRandomValues()会把随机值写入作为参数传给它的定型数组。要使用CSPRNG重新实现Math.random(),可以通过生成一个随机的32位数值,然后用它去除最大的可能值0xFFFFFFFF。这样就会得到一个介于0和1之间的值:
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
2.生成密码学摘要
计算数据的密码学摘要是非常常用的密码学操作。这个规范支持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"指定。通常,在使用时,二进制的消息摘要会转换为十六进制字符串格式。通过将二进制数据按8位进行分割,然后再调用toString(16)就可以把任何数组缓冲区转换为十六进制字符串:
(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
软件公司通常会公开自己软件二进制安装包的摘要,以便用户验证自己下载到的确实是该公司发布的版本(而不是被恶意软件篡改过的版本)。
3.生成秘钥
SubtleCrypto对象使用CryptoKey类的实例来生成密钥。CryptoKey类支持多种加密算法,允许
控制密钥抽取和使用。除了 digest(),在SubtleCrypto API中所有加密功能都会使用密钥,并使用CryptoKey对象表示加密密钥。要执行签名和加密操作, 请将 CryptoKey 对象传参给 sign() 或 encrypt() 函数。
使用SubtleCrypto.generateKey()方法可以生成随机CryptoKey,这个方法返回一个期约,解决为一个或多个CryptoKey实例。使用时需要给这个方法传入一个指定目标算法的参数对象、一个表示密钥是否可以从CryptoKey对象中提取出来的布尔值,以及一个表示这个密钥可以与哪个SubtleCrypto方法一起使用的字符串数组(keyUsages)。
假设要生成一个满足如下条件的对称密钥:
- 支持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)}
})();
4.导出、导入秘钥
如果密钥是可提取的,那么就可以在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()的组合。下面的方法会生成密钥、导出密钥,然后再导入密钥
(async function() {
const params = {
name: 'AES-CTR',
length: 128
};
const keyUsages = ['encrypt', 'decrypt'];
const keyFormat = 'raw';
const isExtractable = true;
const key = await crypto.subtle.generateKey(params, isExtractable,
keyUsages);
const rawKey = await crypto.subtle.exportKey(keyFormat, key);
const importedKey = await crypto.subtle.importKey(keyFormat, rawKey,
params.name,
isExtractable, keyUsages);
console.log(importedKey);
// CryptoKey {type: "secret", extractable: true, algorithm: {...},
usages: Array(2)}
})();
5.使用非对称密钥签名和验证消息
通过SubtleCrypto对象可以使用公钥算法用私钥生成签名,或者用公钥验证签名。这两种操作分别通过SubtleCrypto.sign()和SubtleCrypto.verify()方法完成。签名消息需要传入参数对象以指定算法和必要的值、CryptoKey和要签名的ArrayBuffer或ArrayBufferView。下面的例子会生成一个椭圆曲线密钥对,并使用私钥签名消息,使用公钥验证消息:
(async function() {
const keyParams = {
name: 'ECDSA',
namedCurve: 'P-256'
};
const keyUsages = ['sign', 'verify'];
const {publicKey, privateKey} = await
crypto.subtle.generateKey(keyParams, true,
keyUsages);
const message = (new TextEncoder()).encode('I am Satoshi Nakamoto');
const signParams = {
name: 'ECDSA',
hash: 'SHA-256'
};
const signature = await crypto.subtle.sign(signParams, privateKey,
message);
const verified = await crypto.subtle.verify(signParams, publicKey,
signature,
message);
console.log(verified); // true
})();
6.使用对称密钥加密和解密
SubtleCrypto对象支持使用公钥和对称算法加密和解密消息。这两种操作分别通过SubtleCrypto.encrypt()和SubtleCrypto.decrypt()方法完成。加密消息需要传入参数对象以指定算法和必要的值、加密密钥和要加密的数据。下面的例子会生成对称AES-CBC密钥,用它加密消息,最后解密消息:
(async function() {
const algoIdentifier = 'AES-CBC';
const keyParams = {
name: algoIdentifier,
length: 256
};
const keyUsages = ['encrypt', 'decrypt'];
const key = await crypto.subtle.generateKey(keyParams, true,
keyUsages);
const originalPlaintext = (new TextEncoder()).encode('I am Satoshi
Nakamoto');
const encryptDecryptParams = {
name: algoIdentifier,
iv: crypto.getRandomValues(new Uint8Array(16))
};
const ciphertext = await crypto.subtle.encrypt(encryptDecryptParams,
key,
originalPlaintext);
console.log(ciphertext);
// ArrayBuffer(32) {}
const decryptedPlaintext = await
crypto.subtle.decrypt(encryptDecryptParams, key,
ciphertext);
console.log((new TextDecoder()).decode(decryptedPlaintext));
// I am Satoshi Nakamoto
})();
5.包装和解包密钥
SubtleCrypto对象支持包装和解包密钥,以便在非信任渠道传输。这两种操作分别通过SubtleCrypto.wrapKey()和SubtleCrypto.unwrapKey()方法完成。
包装密钥需要传入一个格式字符串、要包装的CryptoKey实例、要执行包装的CryptoKey,以及一个参数对象用于指定算法和必要的值。下面的例子生成了一个对称AES-GCM密钥,用AES-KW来包装这个密钥,最后又将包装的密钥解包:
(async function() {
const keyFormat = 'raw';
const extractable = true;
const wrappingKeyAlgoIdentifier = 'AES-KW';
const wrappingKeyUsages = ['wrapKey', 'unwrapKey'];
const wrappingKeyParams = {
name: wrappingKeyAlgoIdentifier,
length: 256
};
const keyAlgoIdentifier = 'AES-GCM';
const keyUsages = ['encrypt'];
const keyParams = {
name: keyAlgoIdentifier,
length: 256
};
const wrappingKey = await crypto.subtle.generateKey(wrappingKeyParams,
extractable,
wrappingKeyUsages);
console.log(wrappingKey);
// CryptoKey {type: "secret", extractable: true, algorithm: {...},
usages: Array(2)}
const key = await crypto.subtle.generateKey(keyParams, extractable,
keyUsages);
console.log(key);
// CryptoKey {type: "secret", extractable: true, algorithm: {...},
usages: Array(1)}
const wrappedKey = await crypto.subtle.wrapKey(keyFormat, key,
wrappingKey,
wrappingKeyAlgoIdentifier);
console.log(wrappedKey);
// ArrayBuffer(40) {}
const unwrappedKey = await crypto.subtle.unwrapKey(keyFormat,
wrappedKey,
wrappingKey, wrappingKeyParams, keyParams, extractable,
keyUsages);
console.log(unwrappedKey);
// CryptoKey {type: "secret", extractable: true, algorithm: {...},
usages: Array(1)}
})()
总结
JavaScript原生语言为我们提供了非常实用且基础的密码相关API,我们在日常开发中,大部分的项目可能并不需要接触到这些加密解密的API,但是不妨碍我们对于这些API的学习,万一用到了呢。常规的加密使用md5,sha1等简单加密一下就可以了,只有对安全要求严格的领域才会需要用到严格对称、非对称加密。另外值得一提的是,虽然JavaScript提供的这些API方便实用,但是在性能上表现并不佳,浏览器支持上也并不充分,在项目实施上往往还需要借助第三方库。