全网最详细的HTTP加密算法实现

433 阅读21分钟

大家好,我是每天坚持一点点的铁蛋儿,一个爱分享前端的老body。这篇是分享HTTP加密算法的实现,大家务必点赞收藏保存一下慢慢看,你们的关注是我更新的动力。

如果对HTTP基础原理还不是很了解(比如五层模型、TCP、UDP、为什么握手需要3次、HTTP2的都做了些什么...)下面有完整http基础原理视频。

相关学习资源

本系列有配套视频,大家学习同时千万不要忘了三连 + 关注 + 分享,有道是喝水不忘挖井人~

1. HTTP

http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。

可以把http通信比喻成寄送信件一样,A给B寄信,信件在寄送过程中,会经过很多的邮递员之手,他们可以拆开信读取里面的内容(因为http是明文传输的)。A的信件里面的任何内容(包括各类账号和密码)都会被轻易窃取。除此之外,邮递员们还可以伪造或者修改信件的内容,导致B接收到的信件内容是假的。

比如常见的,在http通信过程中,“中间人”将广告链接嵌入到服务器发给用户的http报文里,导致用户界面出现很多不良链接; 或者是修改用户的请求头URL,导致用户的请求被劫持到另外一个网站,用户的请求永远到不了真正的服务器。这些都会导致用户得不到正确的服务,甚至是损失惨重。

主要存在三大风险:

  1. 窃听风险

    中间人可以获取到通信内容,由于内容是明文,所以获取明文后有安全风险。

  2. 篡改风险

    中间人可以篡改报文内容后再发送给对方,风险极大。

  3. 冒充风险

    比如你以为是在和某宝通信,但实际上是在和一个钓鱼网站通信。

2. HTTPS

我们来看下HTTPS是如何解决HTTP带来的问题,首先要解决http带来的问题,就要引入加密以及身份验证机制。

如果Server(以后简称服务器)给Client(以后简称 客户端)的消息是密文的,只有服务器和客户端才能读懂,就可以保证数据的保密性。同时,在交换数据之前,验证一下对方的合法身份,就可以保证通信双方的安全。

那么,问题来了,服务器把数据加密后,客户端如何读懂这些数据呢?

这时服务器必须要把加密的密钥(对称密钥,后面会详细说明)告诉客户端,客户端才能利用对称密钥解开密文的内容。

图上就是对称加密算法,其中图中的密钥S同时扮演加密和解密的角色。

但是,服务器如果将这个对称密钥以明文的方式给客户端,还是会被中间人截获,中间人也会知道对称密钥,依然无法保证通信的保密性。如果服务器以密文的方式将对称密钥发给客户端,客户端又如何解开这个密文,得到其中的对称密钥呢?

有人说对这个密钥加密不就完了,但对方如果要解密这个密钥还是要传加密密钥给对方,依然还是会被中间人截获的,这么看来直接传输密钥无论怎样都无法摆脱鸡生蛋蛋生鸡的问题,肯定是不可行的。

其实https是通过一下三点实现的数据加密安全:

  1. 非对称加密算法(公钥和私钥)交换对称密钥
  2. 数字证书验证身份(验证公钥是否是伪造的
  3. 利用对称密钥加解密后续传输的数据

请小伙伴们先记住这三个点接下来我一个个详细展开来说

首先第一点 非对称加密算法(公钥和私钥)交换对称密钥, 说到非对称算法,我们得先了解下对称算法。

对称加密算法:

对称加密是指:加密和解密使用相同密钥的算法。它要求发送方和接收方在安全通信之前,商定一个对称密钥。

对称算法的安全性完全依赖于密钥,密钥泄漏就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信至关重要。

对称加密又分为两种模式:

  1. 流加密

    流加密是将消息作为字节流对待,并且使用数学函数分别作用在每一个字节位上。使用流加密时,每加密一次,相同的明文位会转换成不同的密文位。流加密使用了密钥流生成器,它生成的字节流与明文字节流进行异或,从而生成密文。

  2. 分组加密

    分组加密是将消息划分为若干个分组,这些分组随后会通过数学函数进行处理,每次一个分组。假设使用64位的分组密码,此时如果消息长度为640位,就会被划分成10个64位的分组(如果最后一个分组长度不到64,则用0补齐之后加到64位),每个分组都用一系列数学公式进行处理,最后得到10个加密文本分组。

    然后,将这条密文消息发送给对端。对端必须拥有相同的分组密码,以相反的顺序对10个密文分组使用前面的算法解密,最终得到明文消息。比较常用的分组加密算法有DES、3DES、AES。其中DES是比较老的加密算法,现在已经被证明不安全。

    而3DES是一个过渡的加密算法,相当于在DES基础上进行三重运算来提高安全性,但其本质上还是和DES算法一致。而AES是DES算法的替代算法,是现在最安全的对称加密算法之一。

假如登入系统时需要输入账号密码,当然,校验用户输入的密码本身就是一种对称加密,用户必须输入的密码必须和你之前设置的账号密码相同。

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

基础版:

// 对称加密
let secret = 123
// 加密和解密的密钥是同一个
// 加密
function encrpyt(message) {
  let buffer = Buffer.from(message)
  for (let i = 0; i < buffer.length; i++) {
    buffer[i] = buffer[i] + secret
  }
  return buffer.toString()
}
// 解密
function decrpyt(message) {
  let buffer = Buffer.from(message)
  for (let i = 0; i < buffer.length; i++) {
    buffer[i] = buffer[i] - secret
  }
  return buffer.toString()
}
let message = 'tiedan'
let encrpytMessage = encrpyt(message)
console.log('加密' + encrpytMessage)
let decrypMeassage = decrpyt(encrpytMessage)
console.log('解密' + decrypMeassage)

使用 nodejs 来进行对称加密:

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

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

// 使用crypto模块进行加密
let crypto = require('crypto')
// 加密
function encrypto(data, key, iv) {
  let cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
  cipher.update(data)
  return cipher.final('hex') // 转成16进制的意思 把结果输出成16进制的字符串
}
// 解密
function decrpyto(data, key, iv) {
  let cipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
  cipher.update(data, 'hex') // 告诉他添加的是16进制的字符串数据
  return cipher.final('utf8') // 输出成utf8的数据
}
let key = '1234567890123456'
let iv = '1234567890123456'
let data = '铁蛋儿'
let encryptoData = encrypto(data, key, iv)
console.log('加密的结果:' + encryptoData)
let decrpytoData = decrpyto(encryptoData, key, iv)
console.log('解密的结果:' + decrpytoData)

总结对称加密算法的优缺点:

优点: 计算量小、加密速度快、加密效率高。

缺点:

  1. 交易双方都使用同样密钥,安全性得不到保证;
  2. 每次使用对称加密算

非对称加密算法:

非对称加密用的是一对秘钥,分别叫做公钥(public key)和私钥(private key),也叫非对称秘钥。

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

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

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

非对称加密即加解密双方使用不同的密钥,一把作为公钥,可以公开的,一把作为私钥,不能公开,公钥加密的密文只有私钥可以解密,私钥加密的内容,也只有公钥可以解密。

这样的话对于 server 来说,保管好私钥,发布公钥给其他 client, 其他 client 只要把对称加密的密钥加密传给 server 即可,如此一来由于公钥加密只有私钥能解密,而私钥只有 server 有,所以能保证 client 向 server 传输是安全的,server 解密后即可拿到对称加密密钥,这样交换了密钥之后就可以用对称加密密钥通信了。

非对称密钥交换算法本身非常复杂,密钥交换过程涉及到随机数生成,模指数运算,空白补齐,加密,签名等等一系列极其复杂的过程,常见的密钥交换算法有RSA,ECDHE,DH,DHE等算法。涉及到比较复杂的数学问题。其中,最经典也是最常用的是RSA算法。

RSA:诞生于1977年,经过了长时间的破解测试,算法安全性很高,最重要的是,算法实现非常简单。缺点就是需要比较大的质数(目前常用的是2048位)来保证安全强度,极其消耗CPU运算资源。RSA是目前唯一一个既能用于密钥交换又能用于证书签名的算法,RSA 是最经典,同时也是最常用的是非对称加解密算法。

3.png

实现一个简单版的RSA非对称加密算法:


// 两个质数相乘得到一个结果  正向很容易 逆向不可能
let p = 3, q = 11
let N = p * q
let fN = (p - 1) * (q - 1)// 欧拉函数

let e = 7
// e*d%fN !==1 说名找到了私钥

for (var d = 1; e * d % fN !== 1; d++) {
  d++;
}
console.log(d)

let data = 5

let c = Math.pow(data, e) % N

console.log('加密后:' + c)

let m = Math.pow(c, d) % N

console.log('解密后:' + m)

实现使用nodejs 来进行非对称加密:

const crypto = require('crypto')
// 密钥加密的短语
const passphrase = 'tiedan66666'
// rsa 非对称加密
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 1024, // 指定密钥的长度
  // 公钥编码格式
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  // 私钥编码格式
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase
  }
})

// 加密
const encrypto = (publicKey, string) => {
  return crypto.publicEncrypt({ key: publicKey, passphrase }, Buffer.from(string)).toString('hex')
}
// 解密
const decrypto = (privateKey, encryptoString) => {
  return crypto.privateDecrypt({ key: privateKey, passphrase }, Buffer.from(encryptoString, 'hex'))
}
const string = 'tiedan'

const encryptoString = encrypto(publicKey, string)
console.log('公钥加密后的结果:' + encryptoString)


const decryptoString = decrypto(privateKey, encryptoString)
console.log('私钥解密后的结果:' + decryptoString)

非对称加密相比对称加密更加安全,但也存在两个致命的缺点:

  1. CPU计算资源消耗非常大。一次完全TLS握手,密钥交换时的非对称解密计算量占整个握手过程的90%以上。而对称加密的计算量只相当于非对称加密的0.1%。如果后续的应用层数据传输过程也使用非对称加解密,那么CPU性能开销太庞大,服务器是根本无法承受的。赛门特克给出的实验数据显示,加解密同等数量的文件,非对称算法消耗的CPU资源是对称算法的1000倍以上。
  2. 非对称加密算法对加密内容的长度有限制,不能超过公钥长度。比如现在常用的公钥长度是2048位,意味着待加密内容不能超过256个字节。

所以非对称加解密(极端消耗CPU资源)目前只能用来作对称密钥交换或者CA签名,不适合用来做应用层内容传输的加解密。

3. 数字证书

如何得到公钥?

如果使用非对称加密算法,客户端需要一开始就持有公钥,要不没法开展加密行为啊。

如何让A、B客户端安全地得到公钥?

  1. 服务器端将公钥发送给每一个客户端
  2. 服务器端将公钥放到一个远程服务器,客户端可以请求得到

选择方案1,因为方案2又多了一次请求,还要另外处理公钥的放置问题。

公钥被调包了怎么办?又是一个鸡生蛋蛋生鸡问题?

但是方案1有个问题:如果服务器端发送公钥给客户端时,被中间人调包了,怎么办?

使用第三方机构的公钥解决鸡生蛋蛋生鸡问题

公钥被调包的问题出现,是因为客户端无法分辨返回公钥的人到底是中间人,还是真的服务器。这其实就是密码学中提的身份验证问题。

问题的难点是如果选择直接将公钥传递给客户端的方案,始终无法解决公钥传递被中间人调包的问题。

所以不能直接将服务器的公钥传递给客户端,而是第三方机构使用它的私钥对我们的公钥进行加密后,再传给客户端。客户端再使用第三方机构的公钥进行解密。

下图就是我们设计的第一版“数字证书”,证书中只有服务器交给第三方机构的公钥,而且这个公钥被第三方机构的私钥加密了:

第一版数字证书的内容

如果能解密,就说明这个公钥没有被中间人调包。因为如果中间人使用自己的私钥加密后的东西传给客户端,客户端是无法使用第三方的公钥进行解密的。

开始引入第三方机构

第三方机构不可能只给你一家公司制作证书,它也可能会给中间人这样有坏心思的公司发放证书。这样的,中间人就有机会对你的证书进行调包,客户端在这种情况下是无法分辨出是接收的是你的证书,还是中间人的。因为不论中间人,还是你的证书,都能使用第三方机构的公钥进行解密。像下面这样:

第三方机构向多家公司颁发证书的情况:第三方机构向多家公司颁发证书的情况

客户端能解密同一家第三机构颁发的所有证书:客户端能解密同一家第三机构颁发的所有证书

最终导致其它持有同一家第三方机构证书的中间人可以进行调包:

证书依然可以被中间人调包

数字签名,解决同一机构颁发的不同证书被篡改问题

要解决这个问题,首先要想清楚一个问题,辨别同一机构下不同证书的这个职责,我们应该放在哪?

只能放到客户端了。意思是,客户端在拿到证书后,自己就有能力分辨证书是否被篡改了。如何才能有这个能力呢?

比如你是HR,你手上拿到候选人的学历证书,证书上写了持证人,颁发机构,颁发时间等等,同时证书上,还写有一个最重要的:证书编号!我们怎么鉴别这张证书是的真伪呢?只要拿着这个证书编号上相关机构去查,如果证书上的持证人与现实的这个候选人一致,同时证书编号也能对应上,那么就说明这个证书是真实的。

可是,这个“第三方机构”到底是在哪呢?是一个远端服务?不可能吧?如果是个远端服务,整个交互都会慢了。所以,这个第三方机构的验证功能只能放在客户端的本地了。

客户端本地怎么验证证书呢?

客户端本地怎么验证证书呢?答案是证书本身就已经告诉客户端怎么验证证书的真伪。

也就是证书上写着如何根据证书的内容生成证书编号。客户端拿到证书后根据证书上的方法自己生成一个证书编号,如果生成的证书编号与证书上的证书编号相同,那么说明这个证书是真实的。

同时,为避免证书编号本身又被调包,所以使用第三方的私钥进行加密。

1、数字签名的签发。首先是使用哈希函数对待签名内容进行安全哈希,生成消息摘要,然后使用CA自己的私钥对消息摘要进行加密。

2、数字签名的校验。使用CA的公钥解密签名,然后使用相同的签名函数对签名证书内容进行签名,并和服务端数字签名里的签名内容进行比较,如果相同就认为校验成功

但是第三方机构的公钥怎么跑到了客户端的机器中呢?世界上这么多机器。

其实呢,现实中,浏览器和操作系统都会维护一个权威的第三方机构列表(包括它们的公钥)。因为客户端接收到的证书中会写有颁发机构,客户端就根据这个颁发机构的值在本地找相应的公钥。

题外话:如果浏览器和操作系统这道防线被破了,就没办法。想想当年自己装过的非常规XP系统,都害怕。

说到这里,想必大家已经知道上文所说的,证书就是HTTPS中数字证书,证书编号就是数字签名,而第三方机构就是指数字证书签发机构(CA)。

大家要考虑两个问题

问题一、 如何验证证书的真实性,如何防止证书被篡改

想象一下上文中我们提到的学历,企业如何认定你提供的学历证书是真是假呢,答案是用学历编号,企业拿到证书后用学历编号在学信网上一查就知道证书真伪了,学历编号其实就是我们常说的数字签名,可以防止证书造假。

回到 HTTPS 上,证书的数字签名该如何产生的呢,一图胜千言

  1. 首先使用一些摘要算法(如 MD5)将证书明文(如证书序列号,DNS主机名等)生成摘要,然后再用第三方权威机构的私钥对生成的摘要进行加密(签名)

    消息摘要是把任意长度的输入揉和而产生长度固定的伪随机输入的算法,无论输入的消息有多长,计算出来的消息摘要的长度总是固定的,一般来说,只要内容不同,产生的摘要必然不同(相同的概率可以认为接近于 0),所以可以验证内容是否被篡改了。

// MD5 哈希算法
let crypto = require('crypto')
let content = '1231'
let md5Hash = crypto.createHash('md5').update(content).update(content).digest('hex')
console.log('md5Hash', md5Hash, md5Hash.length)

// sha256
let content1 = '1'
let sha1Hash = crypto.createHmac('sha256', content1).digest('hex')
console.log('sha1Hash', sha1Hash, sha1Hash.length)

为啥要先生成摘要再加密呢,不能直接加密?

因为使用非对称加密是非常耗时的,如果把整个证书内容都加密生成签名的话,客户端验验签也需要把签名解密,证书明文较长,客 户端验签就需要很长的时间,而用摘要的话,会把内容很长的明文压缩成小得多的定长字符串,客户端验签的话就会快得多。

  1. 客户端拿到证书后也用同样的摘要算法对证书明文计算摘要,两者一笔对就可以发现报文是否被篡改了,那为啥要用第三方权威机构(Certificate Authority,简称 CA)私钥对摘要加密呢,因为摘要算法是公开的,中间人可以替换掉证书明文,再根据证书上的摘要算法计算出摘要后把证书上的摘要也给替换掉!这样 client 拿到证书后计算摘要发现一样,误以为此证书是合法就中招了。所以必须要用 CA 的私钥给摘要进行加密生成签名,这样的话 client 得用 CA 的公钥来给签名解密,拿到的才是未经篡改合法的摘要(私钥签名,公钥才能解密)

    server 将证书传给 client 后,client 的验签过程如下

    这样的话,由于只有 CA 的公钥才能解密签名,如果客户端收到一个假的证书,使用 CA 的公钥是无法解密的,如果客户端收到了真的证书,但证书上的内容被篡改了,摘要比对不成功的话,客户端也会认定此证书非法。

    细心的你一定发现了问题,CA 公钥如何安全地传输到 client ?如果还是从 server 传输到 client,依然无法解决公钥被调包的风险,实际上此公钥是存在于 CA 证书上,而此证书(也称 Root CA 证书)被操作系统信任,内置在操作系统上的,无需传输,如果用的是 Mac 的同学,可以打开 keychain 查看一下,可以看到很多内置的被信任的证书。

问题二、 如何防止证书被调包

实际上任何站点都可以向第三方权威机构申请证书,中间人也不例外。

正常站点和中间人都可以向 CA 申请证书,获得认证的证书由于都是 CA 颁发的,所以都是合法的,那么此时中间人是否可以在传输过程中将正常站点发给 client 的证书替换成自己的证书呢,如下所示

image-20210721110924140

答案是不行,因为客户端除了通过验签的方式验证证书是否合法之外,还需要验证证书上的域名与自己的请求域名是否一致,中间人中途虽然可以替换自己向 CA 申请的合法证书,但此证书中的域名与 client 请求的域名不一致,client 会认定为不通过!

次完整版实现:

// 证书的实现

let passphrase = 'passphrase'

let { generateKeyPairSync, createHash, createSign, createVerify, verify, } = require('crypto')

// 服务器端的非对称算法
let serverRsa = generateKeyPairSync('rsa', {
  modulusLength: 1024,
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase
  }
})

// CA 非对称算法

let caRsa = generateKeyPairSync('rsa', {
  modulusLength: 1024,
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem',
    cipher: 'aes-256-cbc',
    passphrase
  }
})

// 证书铭文

const info = {
  domain: 'http://127.0.0.1:8080',
  publicKey: serverRsa.publicKey
}

// 签名
let hash = createHash('sha256').update(JSON.stringify(info)).digest('hex')

// CA 私钥加密

let sign = getSign(hash, caRsa.privateKey, passphrase)


// CA 公钥解密
let valid = verifySign(hash, sign, caRsa.publicKey)

console.log(valid)


function getSign(content, privateKey, passphrase) {
  let signObj = createSign('RSA-SHA256');
  signObj.update(content)
  return signObj.sign({
    key: privateKey,
    format: 'pem',
    passphrase
  }, 'hex')
}



function verifySign(content, sign, publicKey) {
  let verifyObj = createVerify('RSA-SHA256');
  verifyObj.update(content)
  return verifyObj.verify(publicKey, sign, 'hex')
}

CA如何颁发数字证书给服务器端的?

我们如何向CA申请呢?每个CA机构都大同小异,我在网上找了一个:

申请数字证书

拿到证书后,我们就可以将证书配置到自己的服务器上了。 那么如何配置?感兴趣的小伙伴可以关注私信我vx:15910703837