乐趣区

关于PHP加解密之终扯到ECDH了API安全加强篇三

其实,前面两篇翻来覆去只为叨逼叨叨逼叨两件事情:

  • 对称加解密,典型算法有 AES、DES、3DES 等等
  • 非对称加解密,典型的算法有 RSA、DSA、ECDH 等等

但是,我知道大家最讨厌在看这种文章的时候冒出来的一坨“椭圆曲线”、“素数”、“质数”等等这样的玩意,反正看也看不懂,理解也理解不了,背也背不过,所以我索性就不写这些玩意,一点儿都不写,不装任何逼(然而实际上我背过了,我最近一直在搞线性代数,所以对数学比原来稍微敏感了一些)。

写到这里后,就有刁民、php 泥腿子自以为已经掌握了高科技,随便从 github 上扒两个库下来跑了跑 test 就开始四处装逼,声称自己精通对称加密算法和非对称加密算法,尤其是在面试的时候,上去就是跟面试官一顿糊弄,糊弄住了就要 5 万,糊弄不了要 5 千。然而我要告诉你的是,你应该接继续往下看,这样的话你在面试的时候,糊弄住了就可以张口要 8 万,糊弄不了也能最低要 8 千!比原来要 5000 整整多了 3000!而且我提供的这份装逼指南还是免费的!

今天我们从一个实际需求作为出发点,比如你是 API 开发人员(当然了,作为只有十来个人的小公司,你还得兼职运维,不过工资只按开发算,运维的活儿算是你友情赞助给老板的),然后老板兼 PM 向你提出了一个比较严峻的问题,大概意思就是“公司的项目是个非常牛逼的项目,一年后公司是要上市的,你必须要加密了数据,让 BAT 和 TMD 都无法抄袭我们!然后你就能买车买房!”,你表示十分认可。由于你已经看过了我前面两篇文章,再加上老板一再强调“我们这个是牛逼的项目,迟早要上市”,所以你就准备用高安全性的非对称加密来解决这个问题。

具体做法就是服务器生成一对公私钥,然后再生成一对公私钥给所有客户端公用。比如用户登陆 API,接口文档大概如下:

API : https://www.so.com/api/user/login
METHOD : POST
PROTOCOL : 将数据以 JSON 形式,全部放入到 http body 体中,key 叫做 mzip
DATA : {
  'username' => 'xitele',
  'password' => 'qiangdadaoniyongyuancaibuchulaishiduoshao'
}

然后客户端执行登陆的伪代码如下:

var username string = 'xitele'
var password string = md5('123456')
// 将数据生成 json
var data = jsonize( hashMap(
  'username' : username,
  'password' : password
) )
// 用服务器公钥,将数据加密
var encryptData = RSA.encrypt('服务端的公钥', data)
// 再次封装数据为 json
var lastJson = jsonize( hashMap('mzip' => encryptData) )
// 提交数据
http.post('https://www.so.com/api/user/login', lastJson, function() {// ... ... do something ...} )

服务器端使用世界上最好的语言来实现的,所以代码你会觉得十分眼熟:

<?php
$jRawData = file_get_contents('php://input');
$aRawData = json_decode($jRawData, true);
// 使用服务器私钥,对 mzip 中的加密数据进行解密
$jDecryptData = RSA::decrypt('服务器的私钥', $aRawData['mzip'] );
// 解密后的数据实际上就是 {"username":"xitele","password":"e10adc3949ba59abbe56e057f20f883e"}
$aDecryptData = json_decode($jDecryptData, true);
// 进入到我们最熟悉的增删改查流程!$pdo = new PDO();
$sth = $pdo->prepare("select * from user where username=:username");
$sth->bindParam(':username', $aDecryptData['username'], PARAM_STR );
$pdo->execute();
$aUser = $pdo->fetch();
if ($aUser['password'] != $aDecryptData['password'] ) {
  echo json_encode( array(
    'code' => 0,
    'msg' => '登陆成功'
  ) );
}
else {
  echo json_encode( array(
    'code' => 10002,
    'msg' => '登陆失败'
  ) );
}

上线后,发现倒也没啥大问题了,就是明显服务器 CPU 负载特别高,客户端也感觉有点儿卡。很明显,非对称加密的 CPU 极大的消耗成了一种瓶颈。于是你找老板申请服务区费用,老板当场表示非常理解,大手一挥就给你批了 300 块钱并表示随意挥霍,把服务器升级成最牛逼的服务器。

当然了,都跟我学习了这么久了你应该马上就意味到 300 块代表着什么,300 块顶多代表能组两个局儿 … …

当然了,API 那里也好交代,全线降级为 AES 对称,CPU 瞬间就下来了,又不是不能用.. …

当然了,300 块组个五人局儿应该还是可以的,除了你和我,再拉上柱子跟老赵,最后再带上陈旭,局儿上除了吃饭,就额外讨论一下关于这个问题的解决方案。

局儿后,我神神秘秘地告诉你说“这特么简单,我给你讲,你服务器先随机生成一个 AES 对称加密用的密钥,然后利用客户端的 RSA 公钥加密后传给客户端,客户端再通过自己的 RSA 私钥解密得到这个 AES 对称密钥,然后再用这个 AES 对称密钥进行后续的加解密即可,然后你可以给这个 AES 密钥设定一个有效期,比如五分钟,当过期后,就再次利用上面的流程申请新的 AES 密钥即可!这样,不仅保证了 AES 密钥的安全,还能解决了性能问题!”

铺垫这么长,终于能扯出来今天的讨论关键点了:密钥协商 / 交换!这就是我们今天的核心话题了。

先说下为什么会出现密钥协商和交换这种玩意,其实就是为了避免密钥在网络上的传输被劫持导致的安全问题,前两句话的潜台词就是“这个世界上存在着一种即便我不告诉你,你也能知道我想告诉你什么的心有灵犀解决方案”。

密钥协商交换一般常用的有如下几种方案:

  • 利用 RSA 等非对称加密技术进行交换,也就是 300 块的局儿上那个方案
  • 利用专门伺候密钥交换需求的交换算法,比如 DH 算法,全称叫做 Diffie-Hellman 密钥交换。Diffie 和 Hellman 分别是两个大叔的名字(注意,此二位是数学家),是他们合伙搞出来的这个算法,DH 算法先于 RSA 出现。

其中,利用非对称加密的方案大概就是我前面说的那样,伪代码已经展示过了。那么 DH 到底是个什么玩意呢?

下面我们玩一个比较简单的数字游戏:

1、元首和古德里安都同时选择 100 这个数字,其他人知不知道无所谓
2、元首随机出了一个数字 9,然后将 9 乘以数字 100,得到 900,其他人能不能知道无所谓
3、古德里安随机出了一个数 3,然后将 3 乘以数字 100,得到 300,其他人能不能知道无所谓
4、元首将 900 扔给古德里安
5、古德里安将 300 扔给元首
到这里后,元首手里有的数据有 100、9、300,古德里安手里的数据有 100、3、900,然后两个人此时只需要默默地做下面这一步:元首:9 * 300 = 2700
古德里安:3 * 900 = 2700
OK 了,就 2700 了 

双方都在仅仅是远远地确认了一下眼神,说了一句话(彼此交换 300 和 900),就已经同时得到 2700 这个相同的数字。辣么,2700 就是双方后面进行通信时候对数据进行加密的密钥了。同样,双方可以为这个密钥算一个过期时间,比如五分钟后,然后过期后重新协商出一个新的即可!而且,即便有其他人知道了双方选择的是 100,也知道了元首给古德里安传了 900,也知道了古德里安给元首穿了 300,然而并没有什么卵用,因为他还是不知道对方最终使用的密钥(也就是 2700)是多少。

当然了,现实中真正的 DH 算法选择公共数字、随机数字可不是这么简单的,而且双方最终计算这个密钥的时候也不会像上面那个例子中那么轻松简单做一下乘法而已。

具体人家怎么算得,我就不写了,反正网上到处都有,而且无论我写出来还是不写出来,反正你们都不看,毕竟,这玩意是数学家要搞的玩意。

RSA 的库前面我从 github 上扒过,也 test 过了,所以 RSA 就不演示了。然而,DH 的咱们一起从 github 上扒一个下来玩玩来验证一下我们刚才讲的简单理论。

PHP 的一个 DH 库,GITHUB 链接:https://github.com/jcink/diff…

<?php
require_once 'diffie-hellman.php';
$dh = new DiffieHellman();

将上述代码保存为 index.php,然后 php index.php 32 执行一下,结果如下,你们感受一下:

我们看到这个库顺带打印了一坨 log,作为从来不研究底层的广大泥腿子来说,我们只需要关注最后一行“Shared Key:101451040”,这个就是服务端和客户端协商出来的密钥了,也就是意味着后面 API 的通信过程中使用 101451040 对数据加解密即可。

好了,以上是 DH 算法。其实,圈里那些仁兄在看到今天标题中含有 DH 的时候心里就应该有数了,这傻逼天天在微信群里安利 ECDH,今儿特么终于看到 DH 两个字母了,总算有点儿眉毛了。辣么,我天天在群里安利的 ECDH 到底是什么玩意。

具体原理怎么回事,反正我这次是真是连背都背不过了,不过,你可以简单认为 ECDH 是 DH 的升级版本,毕竟多了两个字母。其实 ECDH 是 ECC 算法和 DH 算法二合一体,妈蛋,又特么冒出来一个 ECC,好了好了,就当我没说。

然后还是老套路,我们从 github 上扒一个库下来简单跑一下 test,这样以后就可以出去装逼要 8 万工资了,传送门:https://github.com/Querdos/EC…

<?php
require_once './autoloader.php';
use Querdos\lib\ECDHCurve25519;

$xitele   = new ECDHCurve25519();
$gudelian = new ECDHCurve25519();

$xitele->computeSecret($gudelian->getPublic() );
$gudelian->computeSecret($xitele->getPublic() );

// shareKey1 和 shareKey2 就是协商出来的密钥
$shareKey1 = $xitele->getSecret();
echo $shareKey1.PHP_EOL;
$shareKey2 = $gudelian->getSecret();
echo $shareKey2.PHP_EOL;

// 我们用 gmp cmp 来对比是否为同一个密钥
if (0 == gmp_cmp( $shareKey1, $shareKey2) ) {echo "一样".PHP_EOL;}
else {echo "不一样".PHP_EOL;}

// 除此之外,这个 ecdh 库比 dh 那个库多了一个验证数据签名验证,可以检验数据是否被篡改!$msg = "hello world";
$signature = $xitele->signMessage($msg);
if ($gudelian->verifySignature( $signature, $xitele->getPublic(), $msg ) ) {echo "验证数据签名成功".PHP_EOL;}
else {echo "验证数据签名失败".PHP_EOL;}
exit;

将代码保存为 index.php,然后 php index.php 执行结果如下图所示:

通过上面代码我们可以看出来,可以直接背诵一个结论,就是 DH 和 ECDH 都可以实现密钥协商交换,但是 ECDH 还可以对数据进行签名,另一方可以对数据进行验签,从而可以判断出数据在传输过程中是否被篡改!

好了,念念已久的 ECDH 终于入讲了!以后我不会再在群里再叨叨这个了,祝你们幸福。

最近开了一个微信公众号:高性能 API 社区,所有文章都先发这里

退出移动版