常见加密算法及其使用

Posted by Neil Ning on 2024-12-09
学习

常见哈希、签名和加密算法

前言

在日常的工作中,我们或多或少都见过或者听过一些关于计算哈希,签名和加密的算法。这些算法在计算机领域中有着非常重要的地位,不同的算法也都有不同的英文名称,比如MD5、SHA-1、RSA、AES等等。每次看到这些算法都会有一种似曾相识的感觉,但是具体属于哪一类算法,又有什么特点,却很难说清楚。本文将对这些常见的算法进行一个简单的介绍,希望能够理清这些算法之间的关系。文中也会使用Nodejs的crypto模块来演示如何使用这些算法。

哈希算法

首先从最容易理解的哈希算法开始。哈希算法是一种将任意长度的输入数据转换为固定长度的输出数据的算法。它的特点是不同的输入会得到不同的输出,相同的输入,会得到相同的输出,并且输出是不可逆的。也就是说,哈希算法是一种单向的算法,只能从输入得到输出,不能从输出得到输入。

哈希算法最常见的是应用在文件传输中,可以对任意长度的文件数据计算哈希值,以验证文件的完整性。常见的哈希算法有MD5、SHA-1、SHA-256等等。不同的算法得到的哈希值长度不同,比如MD5的哈希值长度是128位,SHA-1的哈希值长度是160位。哈希算法的使用非常简单,代码如下:

1
2
3
4
5
6
7
8
const crypto = require('crypto');

const password = 'qwertyuiop';

const md5 = crypto.createHash('md5');
md5.update(password);
const encryptedPassword = md5.digest('hex');
console.log(encryptedPassword); // 6eea9b7ef19179a06954edd0f6c05ceb

update方法用于输入数据,除了字符串,也可以是Buffer、TypedArray、DataView等数据类型。digest方法用于输出哈希值,参数可以是hexbase64latin1等,表示输出的编码格式。常见的输出格式是hex,即16进制编码。

还可以使用其他的哈希算法,比如SHA-1、SHA-256等,只需要将createHash方法的参数改为对应的算法名称即可。

1
2
3
4
5
6
7
const crypto = require('crypto');

const password = 'qwertyuiop';
const sha = crypto.createHash('sha256');
sha.update(password);
const encryptedPassword = sha.digest('hex');
console.log(encryptedPassword); // 9a900403ac313ba27a1bc81f0932652b8020dac92c234d98fa0b06bf0040ecfd

那如何对文件进行哈希计算呢?可以使用Nodejs的fs模块来读取文件数据,然后计算哈希值。由于文件数据可能比较大,不适合一次性读取,可以使用流的方式来读取文件数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const crypto = require('crypto');
const fs = require('fs');

const hash = crypto.createHash('md5');
const stream = fs.createReadStream('test.png');

stream.on('data', function(data) {
hash.update(data);
});

stream.on('end', function() {
console.log(hash.digest('hex')); // a1fe07317fe10f97cfb2e2a9e65cb46f
});

或者通过下面的方式来简化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const crypto = require('crypto');
const fs = require('fs');

const hash = crypto.createHash('md5');
const stream = fs.createReadStream('test.png');

stream.on('readable', function() {
const data = stream.read();
if (data) {
hash.update(data);
} else {
console.log(hash.digest('hex')); // a1fe07317fe10f97cfb2e2a9e65cb46f
}
});

或者还可以使用pipe方法来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const crypto = require('crypto');
const fs = require('fs');
const { Writable } = require('stream');

const hash = crypto.createHash('md5');
const stream = fs.createReadStream('test.png');
const writable = new Writable({
write(chunk, encoding, callback) {
hash.update(chunk);
callback();
},
final() {
console.log(hash.digest('hex')); // a1fe07317fe10f97cfb2e2a9e65cb46f
}
});
stream.pipe(hash).pipe(writable);

hmac算法

上面的hash算法是单向的,只能用于验证数据的完整性,不能用于验证数据的来源。如果使用hash算法来存储密码,会有hash碰撞的问题。黑客可以使用彩虹表来破解密码。彩虹表是一种预先计算好的hash值和明文的对应关系表。黑客可以通过hash值查找对应的明文,从而破解密码。

为了解决这个问题,可以使用hmac算法。hmac算法是一种带有密钥的哈希算法,可以用于验证数据的完整性和来源。可以看作是上面的md5算法的增强版,hmac算法的全称是Hash-based Message Authentication Code,它的计算方式是将密钥和数据进行混合,然后再计算哈希值。这样,即使数据泄露,黑客也无法伪造数据,因为黑客不知道密钥。他的使用方式和hash算法类似,只是需要传入密钥。

1
2
3
4
5
6
7
8
const crypto = require('crypto');

const secret = 'abcdefg'; // 密钥
const password = 'qwertyuiop';
const hash = crypto.createHmac('sha256', secret);
hash.update(password);
const encryptedPassword = hash.digest('hex');
console.log(encryptedPassword); // 566f3cdadb9e1f4543e0ad030c1996fccb68e530356e4563c75c20a470fb23d1

只要密钥不同,即使相同的数据,也会得到不同的哈希值。这样就可以避免hash碰撞的问题。

1
2
3
4
5
6
7
8
9
10
const crypto = require('crypto');

for (let i = 0; i < 5; i++) {
const secret = crypto.randomBytes(16).toString('hex'); // 密钥
const password = 'qwertyuiop';
const hash = crypto.createHmac('sha1', secret);
hash.update(password);
const encryptedPassword = hash.digest('hex');
console.log(encryptedPassword);
}

大文件流的使用方式和hash算法类似,只是需要传入密钥,这里不再赘述。

对称加密算法

加密算法和上面的哈希算法和hmac算法不同,它是一种双向的算法,可以对数据进行加密和解密。对称加密指的是加密和解密使用相同密钥的算法。常见的对称加密算法有DES、3DES、AES等。

crypto模块使用createCipheriv方法来创建加密对象,使用createDecipheriv方法来创建解密对象。这两个方法都需要传入三个参数,分别是

  1. 算法名称,如aes-128-cbcaes-192-cbcaes-256-cbc等,中间的数字表示密钥长度,cbc表示加密模式。
  2. 密钥,长度和算法有关,比如AES的密钥长度可以是128位、192位、256位。
  3. 初始化向量iv。可以是一个随机数,用于增加加密强度。iv的长度和算法有关,比如AES的iv长度是16字节(128位),DES的iv长度是8字节。(64位)

加密和解密还需要调用update方法和final方法,update方法用于输入数据,final方法用于输出数据。update方法也需要传入三个参数,分别是

  1. 输入数据
  2. 输入数据的编码格式,可以是utf8hexbase64
  3. 输出数据的编码格式,可以是hexbase64

final方法只需要传入输出数据的编码格式即可。

对称加密的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function encrypt(text, key, iv) {
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}

function decrypt(encrypted, key, iv) {
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

const password = 'qwertyuiop'; // 明文密码
const key = '1234567890123456'; // 密钥 128位
const iv = '1234567890123456'; // 向量 128位
const encrypted = encrypt(password, key, iv); // 加密
console.log(encrypted); // 949cb01e787c29f7fa519ab2c9b98791

const decrypted = decrypt(encrypted, key, iv); // 解密
console.log(decrypted === password); // true

以上代码中,encrypt方法用于加密数据,decrypt方法用于解密数据。加密和解密都需要使用相同的密钥和向量。这里的密钥和向量都是固定的,实际使用中,密钥和向量应该是随机的,可以使用crypto.randomBytes方法生成。

非对称加密算法

上面的对称加密算法加密和解密使用相同的密钥,这样就会有一个问题,如何安全地传输密钥呢?如果密钥泄露,那么加密就没有意义了。为了解决这个问题,可以使用非对称加密算法。非对称加密算法使用一对密钥,分别是公钥和私钥,可以用公钥加密数据,私钥解密数据。或者私钥加密数据,公钥解密数据。公钥可以公开,私钥必须保密。常见的非对称加密算法有RSA、DSA、ECC等。

使用非对称加密算法时,需要先生成一对密钥,可以openssl命令生成一对密钥文件,也可以使用crypto模块的generateKeyPair方法生成一对密钥。下面分别介绍这两种方式。

首先是使用openssl命令生成一对密钥文件,可以使用下面的命令生成一对RSA密钥文件。

1
2
openssl genrsa -out private.pem 1024 // 生成私钥文件
openssl rsa -in private.pem -pubout -out public.pem // 生成公钥文件

以上命令生成了PEM格式的私钥和公钥文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat private.pem
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDBO4jlIDpmIniRxMByRpVoEaQXsNjE3CF4wvoa8rbj6yVhESZA
kJMDv/Pgr5Ew9GUbASuT+9UEaSwnqUyatg24u5WnXNqMCsAGl657/sFw2yMvpXs5
gE+P0X8aPLghdy76x/PM0RmhMdVKLjsFmWbTUGUvfBZbJur8bs8E+IEYlwIDAQAB
AoGBAIDtRx6RjUV+NHIWE81reN5x/slrzoYy1gZsGVIHpa2WxF7qgVpM3DqBRagh
nD9MoXUOJ9RaD7wcrEBePmVvmOEAvSv2ucu1UpgFGwIy6i4a6oDmugvDvom4yjLT
l8C5+PEKZp4anXWnAyalqfgQIjJdNiPIbttbPD3dt4P2mRcxAkEA/WN4FtyVATQw
Cq3lKv41OsebzuMaW8szykWbmXoQtSn/t4FJIlRf0orwngA3jTlMA4fIoFTr1SjD
WSjYWlQkbwJBAMM5WjGCL7zMOVRsl2DCpbigf3CXao5G4+pNxWV917OUDZX9mNDs
I7FmqgvfjuH4n9EIgjCT6tNTvfPsyBDpclkCQQCld8hbPY68a4UX5DksGzdNfD4+
G0YCPa9DXrNexTyV4ahRAEdu+KRejEbXFxMv0QPXplsYgHxFBcqTtb2bNylXAkEA
haqXHp7MoLAT8MIJQ68CWM9LcoO56YCQPLTTGxJ2xfXw92mTYDjOl4B7nXWMFxxs
EGuK+EfO2LLVtFXDBhFQwQJAP/Y0GPDlYF5YfrkKjXaI7uWZ/KVnQcYaoaj/Pqrb
4pZ3wTB0RBcdDZz8oJ5xksf4/tpetigQgJlmaS/+vRBQLg==
-----END RSA PRIVATE KEY-----

$ cat public.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBO4jlIDpmIniRxMByRpVoEaQX
sNjE3CF4wvoa8rbj6yVhESZAkJMDv/Pgr5Ew9GUbASuT+9UEaSwnqUyatg24u5Wn
XNqMCsAGl657/sFw2yMvpXs5gE+P0X8aPLghdy76x/PM0RmhMdVKLjsFmWbTUGUv
fBZbJur8bs8E+IEYlwIDAQAB
-----END PUBLIC KEY-----

公钥文件和私钥文件都是文本文件,可以直接读取。其格式必须满足一定的规范,一般都有BEGIN XXXEND XXX开头和结尾,中间是密钥的内容。内容部分每行是64个字符,使用\n换行。编码格式一般是base64编码。

crypto模块的非对称加密使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');
const crypto = require('crypto');

const PUBLIC_KEY = fs.readFileSync('public.pem', 'utf8');
const PRIVATE_KEY = fs.readFileSync('private.pem', 'utf8');

const password = 'qwertyuiop'; // 明文密码
// 使用公钥加密、私钥解密
const encrypted = crypto.publicEncrypt(PUBLIC_KEY, Buffer.from(password));
console.log(encrypted.toString('hex'));
const decrypted = crypto.privateDecrypt(PRIVATE_KEY, encrypted);
console.log(decrypted.toString('utf8') === password); // true

// 使用私钥加密、公钥解密
const encrypted2 = crypto.privateEncrypt(PRIVATE_KEY, Buffer.from(password));
console.log(encrypted2.toString('hex'));
const decrypted2 = crypto.publicDecrypt(PUBLIC_KEY, encrypted2);
console.log(decrypted2.toString('utf8') === password); // true

还可以使用crypto模块的generateKeyPair方法创建公钥和私钥,该方法需要传入三个参数,分别是:

  1. 算法名称,如rsadsaecc
  2. 配置对象,包括modulusLengthpublicExponentnamedCurve
  3. 回调函数,用于接收生成的密钥对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const crypto = require('crypto');

crypto.generateKeyPair('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: 'top secret'
}
}, (err, publicKey, privateKey) => {
if (err) {
console.error('Error generating key pair:', err);
} else {
console.log('Public Key:', publicKey);
console.log('Private Key:', privateKey);
}
});

生成的公钥和私钥可以直接保存到文件中,也可以直接使用。加密和解密的方式和上面的方式一样。

签名算法

还用一种常见的算法是签名算法。签名算法是一种用于验证数据完整性和来源的算法,可以对数据生成数字签名,以防止在传输过程中被篡改。签名算法和哈希算法类似,都是单向不可逆的。不同的是,签名算法需要使用私钥对数据进行签名,然后使用公钥对签名进行验证。且只能通过私钥签名,公钥验证签名。常见的签名算法有RSADSAECDSA等。签名算法最常见的应用场景是数字证书和JWT(JWT规范可参考👉这里)。JWT的第三部分就是签名,用于验证数据的完整性和来源。

Nodejs的crypto模块的签名算法使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
const PUBLIC_KEY = fs.readFileSync('public.pem', 'utf8');
const PRIVATE_KEY = fs.readFileSync('private.pem', 'utf8');
const password = 'qwertyuiop'; // 明文密码
const sign = crypto.createSign('RSA-SHA256') // 创建签名对象, 指定签名算法,常见的签名算法有RSA-SHA256、RSA-SHA512、DSA-SHA1等
sign.update(password); // 指定签名内容
const signature = sign.sign(PRIVATE_KEY, 'hex'); // 生成签名,也可以指定输出格式为'base64'、'base64url'等
console.log(signature);

const verify = crypto.createVerify('RSA-SHA256'); // 创建验证对象, 指定算法,必须和签名算法一致
verify.update(password); // 指定验证内容
const result = verify.verify(PUBLIC_KEY, signature, 'hex'); // 验证签名,也可以指定输入格式为'base64'、'base64url'等
console.log(result); // true

DH算法

DH算法是一种密钥交换算法,该算法不是用来加密数据的,而是在不安全的网络中安全地交换密钥。DH算法的全称是Diffie-Hellman算法,它的原理是通过数学运算构造一个复杂的数学难题,对该数学问题无法在短时间内有效的求解。DH算法的过程是,双方各自生成一对公钥和私钥,然后通过公钥交换,最后通过私钥计算出一个相同的密钥。这个密钥只有双方知道,黑客无法窃取。DH算法的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const crypto = require('crypto');

const dh1 = crypto.createDiffieHellman(512);
const prime = dh1.getPrime(); // 获取素数
const generator = dh1.getGenerator(); // 获取生成器
const publicKey1 = dh1.generateKeys(); // 生成公钥
console.log('Prime:', prime.toString('hex'));
console.log('Generator:', generator.toString('hex'));
console.log('Public Key1:', publicKey1.toString('hex'));

const dh2 = crypto.createDiffieHellman(prime, generator);
const publicKey2 = dh2.generateKeys(); // 生成公钥
console.log('Public Key2:', publicKey2.toString('hex'));

const secret1 = dh1.computeSecret(publicKey2); // 生成密钥
const secret2 = dh2.computeSecret(publicKey1); // 生成密钥
console.log('Secret1:', secret1.toString('hex'));
console.log('Secret2:', secret2.toString('hex'));
console.log(secret1.toString('hex') === secret2.toString('hex')); // true

双方生成相同的密钥之后,就可以使用该密钥通过对称加密算法进行加密通信了。

参考资料