对称加密与非对称加密

3,557 阅读7分钟

加密在编程中的应用的是非常广泛的,尤其是在各种网络协议之中,对称/非对称加密则是经常被提及的两种加密方式。

对称加密

我们平时碰到的绝大多数加密就是对称加密,比如:指纹解锁,PIN 码锁,保险箱密码锁,账号密码等都是使用了对称加密。

对称加密:加密和解密用的是同一个密码或者同一套逻辑的加密方式。

这个密码也叫对称秘钥,其实这个对称和不对称指的就是加密和解密用的秘钥是不是同一个

我在上大学的时候做过一个命令行版的图书馆管理系统作为 C 语言课设。登入系统时需要输入账号密码,当然,校验用户输入的密码本身就是一种对称加密,用户必须输入的密码必须和你之前设置的账号密码相同

当时我选择将账号密码存放在本地文件中,但是如果这个文件被窃取了,而且没有对密码本身进行加密的话密码就泄露了。那么如何对存储在文件中的密码进行加密呢?我当时采取的方式(也可以理解为一种加密算法)是:将用户设置的账号密码的每一个字符取它的码值然后加上一个固定的值比如 6,然后存储的时候存储计算后的码值字符串。用 js 来模拟一下:

// 加密用户密码过程
const sourcePassword = 'abcdef'; // 用户账号密码
// 加密后的存储在本地文件的账号密码
const encryptedPassword = [...sourcePassword].map((char) => char.codePointAt(0) + 6).join();
console.log(`加密后的账号密码是:${encryptedPassword}`) // => 加密后的账号密码是:103,104,105,106,107,108

// 解密过程
const decryptedPassword = encryptedPassword.split(',').map((codePoint) => String.fromCodePoint(codePoint - 6)).join('');
console.log(`解密后的账号密码是:${decryptedPassword}`); // => 解密后的账号密码是:abcdef

上面对用户账号密码加密的过程虽然没有明显涉及到用于加密的密码,但是加密解密用的都是同一套逻辑:取字符的 ascii 码值加 6,其实这也是对称加密。其实也可以理解为加密密码就是加密算法本身,知道了加密算法就能解码出账号密码。

有些软件支持对数据进行加密如 rar 加密的时候需要你输入密码,然后解密的时候需要我们输入设置的加密密码。其实回到到我上面说的那个很简单的加密算法,系统可以设计成让用户可以设置一个用于加密账号密码的加密密码。这里简化为一个数字,然后系统在存储用户账号密码的时候就不是像之前那样每个用户都是加上固定的 6 了,而是加上用户设置的这个数字。现在你这个加密逻辑就可以像 rar 公开压缩算法那样公开,即便是被攻击者知道了加密算法和加密后的账号密码,如果不知道加密密码还是无法获取用户账号密码。这里的加密密码和解密密码用的都是同一个密码,对应到上面那个系统就是 6,所以也是对称加密。

使用 nodejs 来进行对称加密

nodejs 的 crypto 模块是一个专门用于各种加密的模块,可以用来取摘要(hash),加盐摘要(hmac),对称加密,非对称加密等。使用 crypto 进行对称加密很简单,crypto 模块提供了 Cipher 类用于加密数据,Decipher 用于解密。

常见的对称加密算法有DES3DESAESBlowfishIDEARC5RC6,这里演示下使用 AES 算法进行对称加密。

const crypto = require('crypto');

/**
 * 对称加密字符串
 * @param {String} password 用于对称加密的秘钥
 * @param {String} string 被加密的数据
 * @return {String} encryptedString 加密后的字符串
 */
const encrypt = (password, string) => {
  // 使用的对称加密算法是:aes-192-cbc
  const algorithm = 'aes-192-cbc';
  // 生成对称加密秘钥,salt 用于生成秘钥,24 指定秘钥长度是 24 位
  const key = crypto.scryptSync(password, 'salt', 24);
  console.log('key:', key); // => key: <Buffer f0 ca 6c ac 39 3c b3 f9 77 13 0d d9 bc cb dd 9d 86 f7 96 e0 75 53 7f 8a>
  console.log(`秘钥长度: ${key.length}`); // => 秘钥长度: 24

  // 初始化向量
  const iv = Buffer.alloc(16, 0);
  // 获得 Cipher 加密类
  const cipher = crypto.createCipheriv(algorithm, key, iv);

  // utf-8 指定被加密的数据字符编码,hex 指定输出的字符编码
  let encryptedString = cipher.update(string, 'utf8', 'hex');
  encryptedString += cipher.final('hex');

  return encryptedString;
};

const PASSWORD = 'lyreal666';
const encryptedString = encrypt(PASSWORD, '天分不够努力来凑');
console.log(`加密后的数据是:${encryptedString}`); // => 加密后的数据是:1546756bb4e530fc1fbae7fd2cf9aeac0368631b54581a39e5c53ee3172638de 

/**
 * 解密字符串
 * @param {String} password 加密密码
 * @param {String} encryptedString 加密后的字符串
 * @return {String} decryptedString 解密后的字符串
 */
const decrypt = (password, encryptedString) => {
  const algorithm = 'aes-192-cbc';
  // 采用相同的算法生成相同的秘钥
  const key = crypto.scryptSync(password, 'salt', 24);
  const iv = Buffer.alloc(16, 0);
  // 生成 Decipher 解密类
  const decipher = crypto.createDecipheriv(algorithm, key, iv);

  let decryptedString = decipher.update(encryptedString, 'hex', 'utf8');
  decryptedString += decipher.final('utf8');

  return decryptedString;
};

const decryptedString = decrypt(PASSWORD, encryptedString);
console.log(`解密后的数据时:${decryptedString}`); // => 解密后的数据时:天分不够努力来凑

非对称加密

非对称加密用的是一对秘钥,分别叫做公钥(public key)和私钥(private key),也叫非对称秘钥。非对称秘钥既可以用于加密还可以用于认证,咱先聊加密。

加密有一个密码就行了,为啥要整个非对称加密要两个密码呢?

黑人问号.jpg

我相信肯定会有人和我一样有过这样的想法。其实对称加密只要保证加密的密码长度足够长的话,被加密的数据在拿不到密码本身的情况下一般是安全的。但是有个问题就是在实际应用中比如加密网络数据,因为加密和解密使用的是同一个秘钥,所以,服务器和客户端必然是要交换秘钥的,而正是因为非对称秘钥由于有一个交换秘钥这一过程可能会被中间人窃取秘钥,一旦对称加密秘钥被窃取,而且被分析出加密算法的话,那么传输的数据对于中间人来说就是透明的。所以对称加密的致命性缺点就是无法保证秘钥的安全性

那么非对称加密就能保证秘钥的安全性了吗?是的,秘钥可以大胆的公开,被公开的秘钥就叫公钥。非对称加密的秘钥由加密算法计算得出,是成对的,可以被公开的那个秘钥称之为公钥不能公开的那个私有的秘钥叫私钥

在使用 github 等 git 仓库托管平台的时候,我们一般都会配置 ssh 公钥,生成一对秘钥我们可以采用下面的命令:

rsa public/private key pair

上面使用 ssh-keygen 程序指定加密算法为 rsa,在当前目录生成一对秘钥 keykey.pubkey 是私钥,key.pub 是公钥,后缀 pub 全拼明显是 public 嘛。来看一下生成的具体内容:

public/private key content

这个 key.pub 是个公钥,公钥就是被设计来可以随意公开的。我们把这个公钥配置到托管平台,就可以不用每次和 github 通信都要输入密码了。

这对公私钥有一个特点,同时也是非对称加密为什么安全的关键就是:使用秘钥对中的一个秘钥加密,加密后的数据只能通过另一个秘钥解密。也就是说使用一对秘钥中的公钥加密数据,只能通过另一个私钥解密出数据。或者反过来,使用一对秘钥中的私钥进行加密的数据,只能通过另一个公钥解密出来。由此可见,从加密的角度来看,公钥和私钥其实作用是等同的,都可以用于加密或解密,只不过当我们使用非对称秘钥用于加密数据时往往是用公钥进行加密。

在 https 的加密中,加密传输的数据本身使用的是对称加密,加密对称秘钥时使用的非对称加密。整个过程是这样的:server 端先生成一对非对称秘钥,将可以公开的公钥发送给 client 端,client 端也决定此次数据传输使用的对称加密算法和对称秘钥,然后利用 server 端给的公钥,对对称秘钥进行加密传输。server 端接受到 client 端发送的对称加密算法和秘钥后,server 端和 client 端的数据传输都使用这个对称秘钥和算法进行对称加密。整个过程中即便 server 端的公钥被中间人知道了内容,但是没有保存在 server 端的私钥,你是无法破译使用公钥加密的对称秘钥的。公钥原本就是可以被随意公开的,拿到也没用,解密需要的是私钥。非对称加密或者说公钥加密之所以能保证加密安全就是因为私钥是保密不公开的,攻击者没有私钥无法破译

可能会有人有疑问:为什么需要使用非对称加密对对称秘钥加密呢?那是因为交换对称秘钥时可能被第三方窃取,对称秘钥被窃取了那对称加密就没意义了。还有为什么不直接使用非对称加密来加密传输内容而只是加密对称秘钥?非对称加密不是对称加密更安全吗?这就和对称加密与非对称加密的特点有关系了。

非对称加密和对称加密对比

  1. 对称加密是一个秘钥,非对称加密是一对,两个秘钥
  2. 非对称加密比起对称加密更安全,因为不存在秘钥泄露问题,公钥即便被知道也没关系
  3. 由于使用非对称加密在计算上特别复杂,所以一般来说对称加密的加密解密的速度相对于非对称加密快很多
  4. 非对称秘钥还可以用于认证

由于以上第三条,所以在 https 中传输数据时不会使用非对称加密加密传输数据,传输数据时有可能数据本身很大,那样的话非对称加密更耗时了,所以传输数据时不会使用非对称加密的方式加密。

使用 nodejs 演示非对称加密

常见的非对称加密有 RSA、ECC(椭圆曲线加密算法)、Diffie-Hellman、El Gamal、DSA(数字签名用),这里演示一下 RSA 加密。

const crypto = require('crypto');

// 秘钥加密短语
const passphrase = 'lyreal666';

// rsa 指定非对称秘钥算法为 rsa
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 4096, // 指定秘钥长度
  publicKeyEncoding: {
    type: 'spki', // 公钥编码格式
    format: 'pem',
  },
  privateKeyEncoding: {
    type: 'pkcs8', // 私钥编码格式
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase,
  },
});

/**
 * 使用公钥加密
 * @param {String} publicKey 用于对称加密的秘钥
 * @param {String} string 被加密的数据
 * @return {String} encryptedString 加密后的字符串
 */
const encrypt = (publicKey, string) => {
  // 使用公钥加密
  return crypto.publicEncrypt({ key: publicKey, passphrase } , Buffer.from(string)).toString('hex');
};

/**
 * 使用私钥解密字符串
 * @param {String} privateKey 私钥
 * @param {String} encryptedString 加密后的字符串
 * @return {String} decryptedString 解密后的字符串
 */
const decrypt = (privateKey, encryptedString) => {
    return crypto.privateDecrypt({ key: privateKey, passphrase } , Buffer.from(encryptedString, 'hex'));
}

const string = '说好不哭,不爱我就拉倒ヽ(`⌒´)ノ';
const encryptedString = encrypt(publicKey, string);
console.log(`公钥加密后的结果:${encryptedString}`); // => 公钥加密后的结果:caf7535c46146f5...
const decryptedString = decrypt(privateKey, encryptedString);
console.log(`私钥解密后的结果:${decryptedString}`); // => 私钥解密后的结果:说好不哭,不爱我就拉倒ヽ(`⌒´)ノ 

非对称密钥认证

非对称加密有时也叫公钥加密,而非对称秘钥认证也被称为私钥认证。我们说使用非对称秘钥对数据进行认证其实就是说确认一个数据是否有没有被篡改过。非对称秘钥除了用于加密数据,用于认证也是非常广泛的,比如手机 apk 的签名, https 中的证书。

原理很简单:比如现在我要认证一个 apk 的代码是否被串改过,首先准备一对非对称秘钥,一般来自权威机构。官方在打包 apk 时不但包含应用代码,还带上一个签名,这个签名这里简单理解为使用私钥对应用代码的 hash 值加密后的数据。在安装 apk 时,android 系统会提取 apk 中的签名,使用公钥解密签名得到原始应用代码的 hash,然后和原始应用代码的 hash 进行比对,如果内容相同,那么 apk 没有被篡改过。如果 apk 的应用代码被第三方修改了,那么从签名中解密出来的 hash 和应用代码的 hash 肯定是不同的。所以可以起到确保应用代码没有篡改,也就是认证

认证的关键其实是因为签名的存在,签名必须保证能拿到 apk 原始应用代码的 hash。至于如何保证签名没有被篡改不在本文讨论范围。

可能有人看了上面对 apk 认证的过程会有这么一个疑问:使用私钥对内容加密可以达到认证的目的,那能不能使用公钥加密来认证呢?

答案肯定是不能的,如果你使用公钥对内容进行加密,那中间人要篡改你的内容,伪造签名超简单,直接使用公钥对伪造后的内容的 hash 加密就可以了。所以使用非对称秘钥可以用于认证的另一个关键就是私钥是不公开的,中间人没法获取私钥,也就没法伪造签名。

几个疑问

hash 算是加密吗?

我觉得不算,hash 是不可逆的,加密应该是可以根据加密后的数据还原的。

base 64 算是加密吗?

是对称加密,对称秘钥就是 base 64 字符码表。

非对称加密绝对安全吗?

没有什么加密是绝对安全的,非对称加密存在交换公钥时公钥被篡改的问题。

由于我并没有专门学过密码学,以上内容皆由我通过总结以往所学所了解的知识所写,难免会有偏颇之处,甚至说带有很明显的个人观点。欢迎读者在评论处指出文中错误和讨论。

感谢您的阅读,如果对你有所帮助不妨加个关注,点个赞支持一下O(∩_∩)O。

本文为原创内容,首发于个人博客,转载请注明出处。