# 02 | 固若金汤的根本: 对称加密与非对称加密

在上一讲中,我们初步学习了 HTTPS,知道 HTTPS 的安全性是由 TLS 来保证的。

你一定很好奇,它是怎么为 HTTP 增加了机密性、完整性,身份认证和不可否认等特性的呢?

先说说机密性。它是信息安全的基础,缺乏机密性 TLS 就会成为无水之源、无根之木。

实现机密性最常用的手段是 加密(encrypt),就是把消息用某种方式转换成谁也看不懂的乱码,只有掌握 特殊钥匙 的人才能再转换出原始文本。

这里的钥匙就叫做 密钥(key),加密前的消息叫 明文(plain text/clear text),加密后的乱码叫 密文(cipher text),使用密钥还原明文的过程叫 解密(decrypt),是加密的反操作,加密解密的操作过程就是 加密算法

所有的加密算法都是公开的 ,任何人都可以去分析研究,而算法使用的密钥则必须保密 。那么,这个关键的密钥又是什么呢?

由于 HTTPS、TLS 都运行在计算机上,所以密钥就是一长串的数字,但约定俗成的度量单位是 位(bit) ,而不是字节(byte)。比如,说密钥长度是 128,就是 16 字节的二进制串,密钥长度 1024,就是 128 字节的二进制串。

按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密

# 对称加密

对称加密 很好理解,就是指加密和解密时使用的 密钥都是同一个 ,是 对称 的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。

举个例子,你想要登录某网站,只要事先和它约定好使用一个对称密码,通信过程中传输的全是用密钥加密后的密文,只有你和网站才能解密。黑客即使能够窃听,看到的也只是乱码,因为没有密钥无法解出明文,所以就实现了机密性。

img

TLS 里有非常多的对称加密算法可供选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。

AES 的意思是 高级加密标准(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法。

ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法。

# 加密分组模式

对称算法还有一个 分组模式 的概念,它可以让算法用固定长度的密钥加密任意长度的明文 ,把小秘密(即密钥)转化为大秘密(即密文)。

最早有 ECB、CBC、CFB、OFB 等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data) ,在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。

把上面这些组合起来,就可以得到 TLS 密码套件中定义的对称加密算法。

比如,AES128-GCM,意思是密钥长度为 128 位的 AES 算法,使用的分组模式是 GCM;ChaCha20-Poly1305 的意思是 ChaCha20 算法,使用的分组模式是 Poly1305。

你可以用实验环境的 URI /24-1 来测试 OpenSSL 里的 AES128-CBC,在 URI 后用参数 key、plain 输入密钥和明文,服务器会在响应报文里输出加密解密的结果。

https://www.chrono.com/24-1?key=123456

usage: /24-1?key=xxx&plain=xxx

algo  = aes_128_cbc
plain = hello openssl
enc   = 93a024a94083bc39fb2c2b9f5ce27c09
dec   = hello openssl
1
2
3
4
5
6
7
8

后端代码

-- Copyright (C) 2019 by chrono
-- aes encrypt and decrypt

local resty_aes = require "resty.aes"
local resty_str = require "resty.string"

local ffi = require "ffi"
local C = ffi.C

-- try aes_gcm
ffi.cdef[[
const EVP_CIPHER *EVP_aes_128_ccm(void);
const EVP_CIPHER *EVP_aes_128_gcm(void);
]]

local scheme = ngx.var.scheme
if scheme ~= 'https' then
    return ngx.redirect(
        'https://'..ngx.var.host..ngx.var.request_uri, 301)
end

local key = ngx.var.arg_key
local plain = ngx.var.arg_plain or 'hello openssl'
local salt = ngx.var.arg_salt

if not key then
    ngx.status = 400
    return ngx.say('you must submit a key for cipher: '..
            ngx.var.uri .. '?key=xxx&plain=xxx'
            --'?key=xxx&salt=xxx'
            )
end

local cipher

--local gcm_func = C['EVP_aes_128_gcm']()
--if gcm_func then
--    cipher = {size = 128, cipher = 'gcm',
--              method = gcm_func}
--end

local aes_128_cbc_md5 = resty_aes:new(key, salt, cipher)

--local plain = 'hello openssl'
local enc = aes_128_cbc_md5:encrypt(plain)
local dec = aes_128_cbc_md5:decrypt(enc)

ngx.say('usage: ' .. ngx.var.uri .. '?key=xxx&plain=xxx\n')
ngx.say('algo  = aes_128_cbc')
ngx.say('plain = ', plain)
ngx.say('enc   = ', resty_str.to_hex(enc))
ngx.say('dec   = ', dec)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

对 lua 不熟悉,不太能看懂里面的逻辑 salt、cipher 是啥,不明白

# 非对称加密

对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方 ,术语叫 密钥交换

因为在对称加密算法中只要持有密钥就可以解密。如果你和网站约定的密钥在传递途中被黑客窃取,那他就可以在之后随意解密收发的数据,通信过程也就没有机密性可言了。

你或许会说:把密钥再加密一下发过去就好了,但传输「加密密钥的密钥」又成了新问题。这就像是「鸡生蛋、蛋生鸡」,可以无限递归下去。只用对称加密算法,是绝对无法解决密钥交换的问题的。

所以,就出现了非对称加密(也叫公钥加密算法)。

它有两个密钥,一个叫 公钥(public key),一个叫 私钥(private key)。两个密钥是不同的(不对称) ,公钥可以公开给任何人使用,而私钥必须严格保密。

公钥和私钥有个特别的 单向 性,虽然都可以用来加密解密,但 公钥加密后只能用私钥解密 ,反过来,私钥加密后也只能用公钥解密

非对称加密可以解决 密钥交换 的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

img

非对称加密算法的设计要比对称算法难得多,在 TLS 里只有很少的几种,比如 DH、DSA、RSA、ECC 等。

RSA 可能是其中最著名的一个,几乎可以说是非对称加密的代名词,它的安全性基于 整数分解 的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

10 年前 RSA 密钥的推荐长度是 1024,但随着计算机运算能力的提高,现在 1024 已经不安全,普遍认为至少要 2048 位。

ECC(Elliptic Curve Cryptography)是非对称加密里的后起之秀,它基于 椭圆曲线离散对数 的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。

目前比较常用的两个曲线是 P-256(secp256r1,在 OpenSSL 称为 prime256v1)和 x25519。P-256 是 NIST(美国国家标准技术研究所)和 NSA(美国国家安全局)推荐使用的曲线,而 x25519 被认为是最安全、最快速的曲线。

ECC 名字里的椭圆经常会引起误解,其实它的曲线并不是椭圆形,只是因为方程很类似计算椭圆周长的公式,实际的形状更像抛物线,比如下面的图就展示了两个简单的椭圆曲线。

img

两个简单的椭圆曲线:y^2=x^3+7,y^2=x^3-x

比起 RSA,ECC 在安全强度和性能上都有明显的优势。160 位的 ECC 相当于 1024 位的 RSA,而 224 位的 ECC 则相当于 2048 位的 RSA。因为密钥短,所以相应的计算量、消耗的内存和带宽也就少,加密解密的性能就上去了,对于现在的移动互联网非常有吸引力。

实验环境的 URI /24-2 演示了 RSA1024,你在课后可以动手试一下。

https://www.chrono.com/24-2?key=123456

usage: /24-2?plain=xxx

-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALRafyXmEY9wBw/lvedQIjP8ZYPEY45S9pqOGYNyQoXAOVEQIMSv5eo2
rgWFREdp2tw25PDjL6+KF3D7sAPUI1j/Nyxq17xcUrFHskKyNnMKJpxHTDrrZmFD
GDj2oWw4kwRL9+m8mhpcFB0qkkJ66q3eb9bqJA7frZxsCRPDW52VAgMBAAE=
-----END RSA PUBLIC KEY-----

-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQC0Wn8l5hGPcAcP5b3nUCIz/GWDxGOOUvaajhmDckKFwDlRECDE
r+XqNq4FhURHadrcNuTw4y+vihdw+7AD1CNY/zcsate8XFKxR7JCsjZzCiacR0w6
62ZhQxg49qFsOJMES/fpvJoaXBQdKpJCeuqt3m/W6iQO362cbAkTw1udlQIDAQAB
AoGBAKpzuSWlakVJWLNSq4dZeenuCjddvcW+bSknUb+klnB4evM9LesWX1JbeV7o
U962kc186CUuYlwiRANZLEKCFSCqQ50KxotT3lZWdcvcQTh625hIPQAPJ5L3UGjx
I1er83KmDeoxk07wNAjmYrTnYXrRxaknJd6/65ke4XeQarBBAkEA3YJ5zvI+sJTp
JrkKnm9U/kZRMcM0QRQLw2iMR58vXmgl+xSOHdtaHs3fylq/xhCh5HlEUeqOrYmN
G6Ci6p+IMQJBANBvgiN1rHKT7M140rEvwIKJf2W+wU2Sf/VkJS6OE+eGb0tzZTYD
s4g3QLFnqPQrUsZ94NGFi8tQ8fJKbsOWtqUCQQDL0pNi6WTl9x/SkdJDlw4OK4Xq
1EPw3hE07a6m+MMNi6fnMTLUJlL2pVmXSYnNJuDQ6wUCm2JOLJO7KETAv6sBAkEA
orUZGsMmHb8ZkH/rwMMs/PmGiI8y6HIfDxjg6YmhQg+wW262KEcVY5T2HEZ2Hjyf
fjEPSZ99M/Z5GBFAi8/fvQJBAIMGwpXeDRi2GPhxdql1YEh8fanCq0Rz4teee6+m
emH+NTGnX6plyikqghnE8RAoR9TMsXR9Eg/KWvblxXS8/V4=
-----END RSA PRIVATE KEY-----

plain = hello openssl
enc   = 2dd5be865b533f4ca0b627ca05c2f709a50d0ecca808a6b28cd9f0c03f8cce84a8a19e10474226509ed196883231a974ca80ef89c142eb81adbd9e1fb0d0115e88d8fd8fde6dd9a80f5e9ebc284536a858f75e52d236d1f6b459c2a6d23a2d25e264bd9ab7dfbc225cc9ed87923d008fa6c5a87908fe162b7a6d534ee4e389ca
dec   = hello openssl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

来看看后端代码

-- Copyright (C) 2019 by chrono
-- rsa1024 encrypt and decrypt

local resty_rsa = require "resty.rsa"
local resty_str = require "resty.string"

local scheme = ngx.var.scheme
if scheme ~= 'https' then
    return ngx.redirect(
        'https://'..ngx.var.host..ngx.var.request_uri, 301)
end

local plain = ngx.var.arg_plain or 'hello openssl'

--[[
local time = ngx.now()

local rsa_public_key, rsa_priv_key, err = resty_rsa:generate_rsa_keys(1024)
if not rsa_public_key then
    ngx.say('generate rsa keys err: ', err)
end

ngx.say('rsa1024 genkey time = ', ngx.now() - time, 's\n')
--]]

-- 公钥
local rsa_public_key = [[
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALRafyXmEY9wBw/lvedQIjP8ZYPEY45S9pqOGYNyQoXAOVEQIMSv5eo2
rgWFREdp2tw25PDjL6+KF3D7sAPUI1j/Nyxq17xcUrFHskKyNnMKJpxHTDrrZmFD
GDj2oWw4kwRL9+m8mhpcFB0qkkJ66q3eb9bqJA7frZxsCRPDW52VAgMBAAE=
-----END RSA PUBLIC KEY-----
]]
-- 私钥
local rsa_priv_key =[[
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQC0Wn8l5hGPcAcP5b3nUCIz/GWDxGOOUvaajhmDckKFwDlRECDE
r+XqNq4FhURHadrcNuTw4y+vihdw+7AD1CNY/zcsate8XFKxR7JCsjZzCiacR0w6
62ZhQxg49qFsOJMES/fpvJoaXBQdKpJCeuqt3m/W6iQO362cbAkTw1udlQIDAQAB
AoGBAKpzuSWlakVJWLNSq4dZeenuCjddvcW+bSknUb+klnB4evM9LesWX1JbeV7o
U962kc186CUuYlwiRANZLEKCFSCqQ50KxotT3lZWdcvcQTh625hIPQAPJ5L3UGjx
I1er83KmDeoxk07wNAjmYrTnYXrRxaknJd6/65ke4XeQarBBAkEA3YJ5zvI+sJTp
JrkKnm9U/kZRMcM0QRQLw2iMR58vXmgl+xSOHdtaHs3fylq/xhCh5HlEUeqOrYmN
G6Ci6p+IMQJBANBvgiN1rHKT7M140rEvwIKJf2W+wU2Sf/VkJS6OE+eGb0tzZTYD
s4g3QLFnqPQrUsZ94NGFi8tQ8fJKbsOWtqUCQQDL0pNi6WTl9x/SkdJDlw4OK4Xq
1EPw3hE07a6m+MMNi6fnMTLUJlL2pVmXSYnNJuDQ6wUCm2JOLJO7KETAv6sBAkEA
orUZGsMmHb8ZkH/rwMMs/PmGiI8y6HIfDxjg6YmhQg+wW262KEcVY5T2HEZ2Hjyf
fjEPSZ99M/Z5GBFAi8/fvQJBAIMGwpXeDRi2GPhxdql1YEh8fanCq0Rz4teee6+m
emH+NTGnX6plyikqghnE8RAoR9TMsXR9Eg/KWvblxXS8/V4=
-----END RSA PRIVATE KEY-----
]]

ngx.say('usage: ' .. ngx.var.uri .. '?plain=xxx\n')
ngx.say(rsa_public_key)
ngx.say(rsa_priv_key)

local pub, err = resty_rsa:new({ public_key = rsa_public_key })
local priv, err = resty_rsa:new({ private_key = rsa_priv_key })

--local plain = 'hello openssl'
-- 使用公钥加密字符串
local enc = pub:encrypt(plain)
-- 使用私钥解密秘闻得到 加密前的字符串
local dec = priv:decrypt(enc)

ngx.say('plain = ', plain)
ngx.say('enc   = ', resty_str.to_hex(enc))
ngx.say('dec   = ', dec)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

它的使用方式,相对来说比对称加密的代码好理解一点。

# 混合加密

看到这里,你是不是认为可以抛弃对称加密,只用非对称加密来实现机密性呢?

很遗憾,虽然非对称加密没有 密钥交换 的问题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。如果仅用非对称加密,虽然保证了安全,但通信速度有如乌龟、蜗牛,实用性就变成了零。

实验环境的 URI /24-3 对比了 AES 和 RSA 这两种算法的性能,下面列出了一次测试的结果:

https://www.chrono.com/24-3

plain = hello openssl
count = 1000

aes_128_cbc enc/dec 1000 times : 1.00ms, 12.70MB/s

rsa_1024 enc/dec 1000 times : 89.00ms, 146.07KB/s

rsa_1024/aes ratio = 89.00

rsa_2048 enc/dec 1000 times : 526.00ms, 24.71KB/s

rsa_2048/aes ratio = 526.00
1
2
3
4
5
6
7
8
9
10
11
12
13
14

看看后端代码

-- Copyright (C) 2019 by chrono
-- aes encrypt and decrypt

local resty_aes = require "resty.aes"
local resty_rsa = require "resty.rsa"
local resty_str = require "resty.string"

local ffi = require "ffi"
local ffi_null = ffi.null
local ffi_cdef = ffi.cdef
local ffi_typeof = ffi.typeof
local ffi_new = ffi.new
local ffi_C = ffi.C

local scheme = ngx.var.scheme
if scheme ~= 'https' then
    return ngx.redirect(
        'https://'..ngx.var.host..ngx.var.request_uri, 301)
end

if not pcall(ffi_typeof, "struct timeval") then

    ffi_cdef[[
    struct timeval {
        long int tv_sec;
        long int tv_usec;
    };
    ]]

    if jit.os == 'Linux' then
        ffi_cdef[[
        // linux
        int gettimeofday(struct timeval *tv, void *tz);
        ]]
    else
        ffi_cdef[[
        // windows
        int ngx_gettimeofday(struct timeval *tv);
        ]]
    end
end

local function ngx_gettimeofday(tv)
    if jit.os == 'Linux' then
        return ffi_C.gettimeofday(tv, ffi_null)
    else
        return ffi_C.ngx_gettimeofday(tv)
    end
end

local timeval_t = ffi_typeof("struct timeval")
local tm = ffi_new(timeval_t)
local now = ffi_new(timeval_t)

local function clock_start()
    ngx_gettimeofday(tm)
end

local function clock_elasped()
    ngx_gettimeofday(now)
    return (tonumber(now.tv_sec) - tonumber(tm.tv_sec)) * 1000 +
           (tonumber(now.tv_usec) - tonumber(tm.tv_usec)) / 1000
end

local count = tonumber(ngx.var.arg_count or 1000)

---- aes_cbc

local plain = 'hello openssl'
local data_len = #plain * count

ngx.say('plain = ', plain)
ngx.say('count = ', count, '\n')

local key = 'a_key_for_aes'
local aes_128_cbc_md5 = resty_aes:new(key)

--ngx_gettimeofday(tm)
clock_start()

local enc, dec

for i = 1, count do
    enc = aes_128_cbc_md5:encrypt(plain)
    dec = aes_128_cbc_md5:decrypt(enc)
end

--ngx_gettimeofday(now)
local aes_time = clock_elasped()

ngx.print('aes_128_cbc enc/dec ', count, ' times : ')
--ngx.print(tonumber(now.tv_sec) - tonumber(tm.tv_sec), 's ')
--ngx.print(tonumber(now.tv_usec) - tonumber(tm.tv_usec), 'us\n')

--local aes_time = (tonumber(now.tv_sec) * 1000 + tonumber(now.tv_usec) / 1000) -
--                 (tonumber(tm.tv_sec) * 1000 + tonumber(tm.tv_usec) / 1000)
--local aes_time = (tonumber(now.tv_sec) - tonumber(tm.tv_sec)) * 1000 +
--                 (tonumber(now.tv_usec) - tonumber(tm.tv_usec)) / 1000

ngx.say(string.format('%.02fms, %.02fMB/s\n', aes_time, data_len / aes_time / 1024))
ngx.flush(true)
ngx.sleep(0)

-- rsa 1024

local rsa_public_key = [[
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALRafyXmEY9wBw/lvedQIjP8ZYPEY45S9pqOGYNyQoXAOVEQIMSv5eo2
rgWFREdp2tw25PDjL6+KF3D7sAPUI1j/Nyxq17xcUrFHskKyNnMKJpxHTDrrZmFD
GDj2oWw4kwRL9+m8mhpcFB0qkkJ66q3eb9bqJA7frZxsCRPDW52VAgMBAAE=
-----END RSA PUBLIC KEY-----
]]

local rsa_priv_key =[[
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQC0Wn8l5hGPcAcP5b3nUCIz/GWDxGOOUvaajhmDckKFwDlRECDE
r+XqNq4FhURHadrcNuTw4y+vihdw+7AD1CNY/zcsate8XFKxR7JCsjZzCiacR0w6
62ZhQxg49qFsOJMES/fpvJoaXBQdKpJCeuqt3m/W6iQO362cbAkTw1udlQIDAQAB
AoGBAKpzuSWlakVJWLNSq4dZeenuCjddvcW+bSknUb+klnB4evM9LesWX1JbeV7o
U962kc186CUuYlwiRANZLEKCFSCqQ50KxotT3lZWdcvcQTh625hIPQAPJ5L3UGjx
I1er83KmDeoxk07wNAjmYrTnYXrRxaknJd6/65ke4XeQarBBAkEA3YJ5zvI+sJTp
JrkKnm9U/kZRMcM0QRQLw2iMR58vXmgl+xSOHdtaHs3fylq/xhCh5HlEUeqOrYmN
G6Ci6p+IMQJBANBvgiN1rHKT7M140rEvwIKJf2W+wU2Sf/VkJS6OE+eGb0tzZTYD
s4g3QLFnqPQrUsZ94NGFi8tQ8fJKbsOWtqUCQQDL0pNi6WTl9x/SkdJDlw4OK4Xq
1EPw3hE07a6m+MMNi6fnMTLUJlL2pVmXSYnNJuDQ6wUCm2JOLJO7KETAv6sBAkEA
orUZGsMmHb8ZkH/rwMMs/PmGiI8y6HIfDxjg6YmhQg+wW262KEcVY5T2HEZ2Hjyf
fjEPSZ99M/Z5GBFAi8/fvQJBAIMGwpXeDRi2GPhxdql1YEh8fanCq0Rz4teee6+m
emH+NTGnX6plyikqghnE8RAoR9TMsXR9Eg/KWvblxXS8/V4=
-----END RSA PRIVATE KEY-----
]]

local pub, err = resty_rsa:new({ public_key = rsa_public_key })
local priv, err = resty_rsa:new({ private_key = rsa_priv_key })

--ngx_gettimeofday(tm)
clock_start()

for i = 1, count do
    enc = pub:encrypt(plain)
    dec = priv:decrypt(enc)
end

--ngx_gettimeofday(now)
local rsa_time = clock_elasped()

ngx.print('rsa_1024 enc/dec ', count, ' times : ')
--ngx.print(tonumber(now.tv_sec) - tonumber(tm.tv_sec), 's ')
--ngx.print(tonumber(now.tv_usec) - tonumber(tm.tv_usec), 'us\n')

--local rsa_time = (tonumber(now.tv_sec) * 1000 + tonumber(now.tv_usec) / 1000) -
--                 (tonumber(tm.tv_sec) * 1000 + tonumber(tm.tv_usec) / 1000)
--local rsa_time = (tonumber(now.tv_sec) - tonumber(tm.tv_sec)) * 1000 +
--                 (tonumber(now.tv_usec) - tonumber(tm.tv_usec)) / 1000

ngx.say(string.format('%.02fms, %.02fKB/s\n', rsa_time, data_len / rsa_time))
ngx.flush(true)

-- ratio

ngx.say(string.format('rsa_1024/aes ratio = %.02f\n', rsa_time / aes_time))
ngx.flush(true)
ngx.sleep(0)

-- rsa 2048

--[[
local rsa_public_key, rsa_priv_key, err = resty_rsa:generate_rsa_keys(2048)
if not rsa_public_key then
    ngx.say('generate rsa keys err: ', err)
end

ngx.say(rsa_public_key)
ngx.say(rsa_priv_key)
--]]

local rsa_public_key = [[
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAv7ZuHcxFfwPqicpsxXuXmcCE4fauuDNO+FiiM2c6fzgpELP6unyl
UWLqENPkGkQ2NmhG9qvENPB5DQVTxWVeSHGjYh8ap9VahoHTmmgUyx6r9ofi8H3e
k1WcOF7VQlnqzZ9RmVZgFH/jd5m+h9M1FqdDS069MvvcJvjY0iHRHTs4MMNepqv8
blawM7uD4OhXMqCIyjCC6RDznnWExPMRbkN7Nabc2HFUfuK4qXGRUZNHhDMD7Btw
osJjk5qIhDCuWP9KJdDRrglyP2/IxR/U5ee9vNajw/1coX5+AhaLDf06yOnXNu3/
uK965P8kM/SDczm783jWfiCiv3C4vDnZXwIDAQAB
-----END RSA PUBLIC KEY-----
]]

local rsa_priv_key =[[
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAv7ZuHcxFfwPqicpsxXuXmcCE4fauuDNO+FiiM2c6fzgpELP6
unylUWLqENPkGkQ2NmhG9qvENPB5DQVTxWVeSHGjYh8ap9VahoHTmmgUyx6r9ofi
8H3ek1WcOF7VQlnqzZ9RmVZgFH/jd5m+h9M1FqdDS069MvvcJvjY0iHRHTs4MMNe
pqv8blawM7uD4OhXMqCIyjCC6RDznnWExPMRbkN7Nabc2HFUfuK4qXGRUZNHhDMD
7BtwosJjk5qIhDCuWP9KJdDRrglyP2/IxR/U5ee9vNajw/1coX5+AhaLDf06yOnX
Nu3/uK965P8kM/SDczm783jWfiCiv3C4vDnZXwIDAQABAoIBAQC0jkFZaRiOzoZm
7bHRsFwQX2QHWQgmzZPzi65/0Roj1SW/6HIcjuY4J3uhC58KKfIQ/dbP3Of2oACy
BbFm+Nh6TCR/diHpraQLiFxdUOc8gg+dKU/QBgvJIVj3MkGRsxPUQtdcHiBxTh1S
eAcc7wtR4YgcdfT0/oXSYo36IgVLi+gWgRDcQMGwPJx/Qts5WbJlfWmDI+zZ8Z5y
5VvNQCWfgCKmC74oeP3QEJVpU6lZp7nSgSsIM+83CiSs5NSftdCJhS5PA7qicuyT
Qzs/OpB7Vk0axKsmuG9XLMtK/aARdUESqBwuh4LlEcwpLfQS5Z9BW4q5OOH1P0UP
wLORt2TxAoGBAOTEJ/TgiZ789BBKnmlE2lDD19pKGc7hvCOh+3PksTnRji8taeaJ
iFeoAW/S3Zvsb/ODsYhlcmw5QXB8lSW8fUfbiDy+B2ozwO1eGKpPRUT1b+qHetPF
Wb+TT48n9bF5fndaL/mkJxU+wqYPa9YN3fymdc+ey8OSHuPsm/OZjQ65AoGBANaJ
CKmg5iWUVNS555gfTPLnvoUlmGj1UyS8QsrogtS25qtBXGMrHbO1lfzZ3/43MwQx
kT20fr1JIngiodJMKN/4h06v7vUrjDto14vQJrAVVi6soYu4VNN+aFM2Jo+VQtmD
t1HQfnf8CzHaDNcaFyGoUUBDBpLLfB4yWKlFOFzXAoGBANoLAM6NKX7pOLNCfAR4
BOHQGK/NyxV94LXR0Xqf8i/axXu//F0on1R1JJFx2ZmhXP8seY04rDvswqu1gu8J
3hscapkCwsx98ZgNBNNnZO2aRgazBOZOBwHrJXycKLj0xQ57XpjB1iKQxDRFJJJM
e1YxTr8KasrIPjseLXKc2265AoGAe1NnIWwXIT81zNvZoH9N0s0ZnpzQEnYEh7eZ
hd9HZlSGIah/HZrphic6w5HTy+WbdCuyXJBn0xQ5tmniMGwLi0TpM3i7m0Cfan+I
eRz9QHfjhQ1ECHe8e5/NBRi57gxV04h+V4/NQ9gl71Bz1StwZK7HlnNxUe2buhgj
E5txHR0CgYEA0JE8z8kM3qCKO3RqF+xUQbJGeb5oqtRRC1O1sU9Ovc8u863tvghv
3QAa9+WhwW5LtX3Ey5rkBicSEQC2LU1aWiiAnhWdBzsVt8ydto+JYsw7qce0rjHM
NFN6HSMLlAWgq2ggkeT5h/btVflm6EyCIqr7LuXGQ5CqXK9tMaISM6o=
-----END RSA PRIVATE KEY-----
]]

local pub, err = resty_rsa:new({ public_key = rsa_public_key })
local priv, err = resty_rsa:new({ private_key = rsa_priv_key })

--ngx_gettimeofday(tm)
clock_start()

for i = 1, count do
    enc = pub:encrypt(plain)
    dec = priv:decrypt(enc)
end

--ngx_gettimeofday(now)
local rsa_time = clock_elasped()

ngx.print('rsa_2048 enc/dec ', count, ' times : ')

--local rsa_time = (tonumber(now.tv_sec) * 1000 + tonumber(now.tv_usec) / 1000) -
--                 (tonumber(tm.tv_sec) * 1000 + tonumber(tm.tv_usec) / 1000)
--local rsa_time = (tonumber(now.tv_sec) - tonumber(tm.tv_sec)) * 1000 +
--                 (tonumber(now.tv_usec) - tonumber(tm.tv_usec)) / 1000

ngx.say(string.format('%.02fms, %.02fKB/s\n', rsa_time, data_len / rsa_time))

-- ratio
ngx.say(string.format('rsa_2048/aes ratio = %.02f\n', rsa_time / aes_time))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

这个代码有点复杂了,可以得到两个信息:

  • 使用不同的加密算法对同一个字符串(明文)进行加密解密,得到消耗的时间
  • 使用明文长度 / 消耗的时间,得到性能指标

可以看到,RSA 的运算速度是非常慢的,2048 位的加解密大约是 15KB/S(微秒或毫秒级),而 AES128 则是 13MB/S(纳秒级),差了几百倍

那么,是不是能够 把对称加密和非对称加密结合起来 呢,两者互相取长补短,即能高效地加密解密,又能安全地密钥交换。

这就是现在 TLS 里使用的 混合加密 方式,其实说穿了也很简单:

  1. 在通信刚开始的时候使用非对称算法 ,比如 RSA、ECDHE,首先解决密钥交换的问题
  2. 然后用随机数产生对称算法使用的 会话密钥(session key),再用 公钥加密 。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
  3. 对方拿到密文后用 私钥解密 ,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

img

这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性

不过这只是「万里长征的第一步」,后面还有完整性、身份认证、不可否认等特性没有实现,所以现在的通信还不是绝对安全,我们下次再说。

# 小结

  1. 加密算法的核心思想是「把一个小秘密(密钥)转化为一个大秘密(密文消息)」,守住了小秘密,也就守住了大秘密;
  2. 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20;
  3. 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC;
  4. 把对称加密和非对称加密结合起来就得到了「又好又快」的混合加密,也就是 TLS 里使用的加密方式。

# 课下作业

  1. 加密算法中「密钥」的名字很形象,你能试着用现实中的锁和钥匙来比喻一下吗?

    加密算法是公开的,好比锁的制造方法是公开的,任何人都可以研究,但是想要开一个锁,只能用某把特定的钥匙,用其他的钥匙是打不开锁的,即想要解密特定的密文,只能用特定的密钥,用其他的密钥是无法解密的

  2. 在混合加密中用到了公钥加密,因为只能由私钥解密。那么反过来,私钥加密后任何人都可以用公钥解密,这有什么用呢?

    解决了身份认证(不可抵赖),私钥少数人知道,比如只有网站,公钥所有访问网站的人都知道,你用公钥加密的密文,只有私钥才能解密,相反的,你至少知道你是和网站对话,并不是和代理或则黑客

# 拓展阅读

  • 严格来说对称加密算法还可以分为 块加密算法( block cipher)流加密算法( stream cipher),DES、AES 等属于块加密,而 RC4、ChaCha20 属于流加密。

  • ECC 虽然定义了公钥和私钥,但不能直接实现密钥交换和身份认证,需要搭配 DH、DSA 等算法,形成专门的 ECDHE、 ECDSA。RSA 比较特殊,本身即支持密钥交换也支持身份认证。

    • 加密交换:不对称,两边所需要的密匙不一致

    • 身份认证:公钥能解密私钥加密的密文,私钥只有持有人有,所以这个是身份认证

      比如你是一个大型网站,你响应给用户的请求,它能解开说明一定是你用私钥加密的

  • 比特币、以太坊等区块链技术里也用到了 ECC,它们选择的曲线是 secp256k1。

  • 由于密码学界普遍不信任 NST 和 NSA,怀疑 secp 系列曲线有潜在的弱点,所以研究出了 x25519 ,它的名字来源于曲线方程里的参数 2^255-19 。另有一个更高强度的曲线 ×448,参数是 2^448-2^224-1

  • 在 Linux上可以使用 OpenSSL 的命令行工具来测试算法的加解密速度,例如 openssl speed aesopenssl speed rsa2048 等。

  • TLS1.2 要求必须实现 TLS_RSA_WITH_AES128_CBC_SHA,TLS1.3 要求必须实现 TLS_AES_128_GCM_SHA256,并且因为前向安全的原因废除了 DH 和 RSA 密钥交换算法。

  • 加密的分组模式到底是什么?

    拿 ECB 来举例子,假设使用 aes128,密钥长度是16 字节,那么就把明文按 16 字节分组,然后每个分组用密钥加密,最后能得到 n 组加密后的密文