SJCL-Crypto-ECDH 会话管理
基本流程
- 服务端启动,使用预制的密钥,生成服务端私钥和公钥对,准备提高服务
- 客户端生成(或恢复)密钥对
访问应用地址,系统检查当前登录状态,如果未登录,系统将重定向到登录页面,或者加载登录模块,此时,客户端生成用于登录和应用的密钥对,或者从存储中恢复(恢复时,获取存储的客户端 ID-CID 和客户端密钥对 CVKEY,并恢复客户端公钥) - 客户端使用公钥访问服务端的公钥接口
- 服务端生成客户端 ID,并使用客户端公钥生成共享密钥,并保存在密钥存储中
- 服务端响应并返回客户端 ID 和服务端公钥
- 客户端获取服务端公钥,并结合客户端私钥计算共享密钥,并存储在 session store 中
- 客户端可选使用使用共享密钥加密请求内容和进行签名,保证信息密码和不可否认性
- 客户端请求时,在请求头中带有客户端 ID
- 服务端处理请求时,通过客户端 ID 和密钥存储获取共享密钥
技术规格
- 服务端加密算法库使用 Nodejs 内置的 crypto
- 客户端加密算法库使用 SJCL
- 所有内容的原始编码均未 UTF-8
- 请求和内容为标准 JSON 对象或编码封装格式
- 密钥编码使用 HEX,不使用 BASE64
- 非对称密钥使用 ECC,选择曲线为 sep256k1/sjcl.ecc.curves.k256
- 对称加密使用 AES-128-CCM,sjcl 的默认加密方式
- 签名使用 HMAC-SHA256
服务端
基于性能的考虑,在比较大的应用规模和强度的环境中,原有的基于 SJCL 服务端实现的 ECC 私钥解密和功能应用的模式已经不太适用,显示为大量并发时,服务器的 CPU 占用率显著提高,并成为请求处理的瓶颈。以此,在放弃在服务端使用 SJCL 的同时,也不考虑使用第三方脚本化的程序库,而优先考虑系统和框架原生程序库。
在 Nodejs 上,使用系统内置原生的安全算法库 crypto。
const clientid = Math.random().toString(36).slice(2,10);
服务端的相关操作流程和实现如下:
引入 crypto 模块
const
crypto = require('crypto')
EC_TYPE = "secp256k1";
crypto 是 nodejs 的标准内置模块,可以直接引用。并同时设置密钥类型为 secp256k1。
创建和设置服务端密钥对
// Server Key store ... const alice_vkey = "451c6de369028a6e6114dcdf4e8f575ae10234818fd91ac1c477bd255af53081"; // create key object const alice = crypto.createECDH(EC_TYPE); // set private key from hex bits alice.setPrivateKey(Buffer.from(alice_vkey,"hex")); // get public key hex string let alice_pkey = alice.getPublicKey("hex");
服务端密钥对象恢复后,可以重复使用
获取客户端公钥生成共享密钥
// from client key add 04 if (cpkey.length==128) cpkey = "04"+cpkey // add from sjcl client let shareKey = alice.computeSecret(Buffer.from(cpkey,"hex")).toString("hex");
服务端信息加密
服务端信息签名
// content prepared const clientid = Math.random().toString(36).slice(2,10); const scontent = JSON.stringify({ cid: clientid, name: "中国" }); // create hmac with type and key update content and digest out put hex let sign2 = crypto.createHmac("sha256",shareKey2).update(scontent).digest("hex"); console.log("Server Signed:",clientid,sign2);
客户端
显然在客户端和浏览器环境中,无法使用 crypto,考虑到 SJCL 在浏览器环境中算是一个非常强大和容易使用的安全算法选项,因此,在浏览器中还是沿用 SJCL,但需要对使用的方式配合服务器端 crypto 来进行相应的调整。
客户端算法的过程和内容包括
引入 SJCL 库
<script src="/js/sjcl.js"></script>
需要注意我们引入的 sjcl 文件需要包含 ECC,我们的版本已经经过精简处理,为 44751 字节。
生成密钥对
// key type const EC_TYPE = sjcl.ecc.curves.k256 // gen pair and get pub and sec let pair = sjcl.ecc.elGamal.generateKeys(EC_TYPE)
要进行 ECDH 计算或者非对称加解密,需要选择 elGamal 算法和对象。
生成的结果为一个密钥对对象,需要对公钥和私钥分别进行序列化,才能够被保存和恢复。
密钥序列化和反序列化
// serialize private key VKEY = sjcl.codec.hex.fromBits(pair.sec.get()) // private key from key string let cprv = new sjcl.ecc.elGamal.secretKey(EC_TYPE, EC_TYPE.field.fromBits(sjcl.codec.hex.toBits(VKEY))) // serialize public key and conact x and y let pub = pair.pub.get() PKEY = sjcl.codec.hex.fromBits(pub.x.concat(pub.y)); // generate public key from key string let apub = new sjcl.ecc.elGamal.publicKey(EC_TYPE, sjcl.codec.hex.toBits(PKEY))
代码中可以看到,公钥序列化时,是将两个公钥参数(x 和 y) 进行了拼接。
需要注意这里私钥和公钥反序列化时,用的方法时不同的。公钥直接使用了字符串,而私钥使用了 field.fromBits 方法
获取公钥
使用标准的 AJAX HTTP GET 方法,并将客户端公钥作为请求参数,可以获取服务端公钥和分配给客户端的 ID。
具体方法略。
计算共享密钥
可以根据本方的私钥,和对方的公钥(从接口获取),计算共享密钥
// cprv is private key object recover from keystring // apub if public key object recover from keystring // compute DH key SHARE_KEY = sjcl.codec.hex.fromBits(cprv.dhJavaEc(apub))
如果两方都是 SJCL,则使用 dh 方法;这里另一方为 crypto 或者 java 系统,需要使用 dhJavaEc 方法。
信息加密
信息签名
可以使用共享密钥对信息原文字符串进行 HMAC 操作,获取该该信息的签名信息。
// create hmac from key with format bits let hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(shareKey2)); // hmac encode content let sign = hmac.encrypt(JSON.stringify({ cid: clientid, name: "中国" })); // convert to hex string sign = sjcl.codec.hex.fromBits(sign); // prepare the content const clientid = Math.random().toString(36).slice(2,10); const scontent = JSON.stringify({ cid: clientid, name: "中国" }); // create hmac from key use utf8 decode let hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(shareKey2)); // encode content default utf8 let sign = hmac.encrypt(scontent); // convert to hex format sign = sjcl.codec.hex.fromBits(sign);
需要注意,虽然使用前面的 hex 共享密钥,但也必须使用 uft-8 进行转化,否则会和服务端产生不一致的情况
在服务器端,使用客户端对应的共享密钥对原文进行相同的操作,并比较操作结果,确认签名有效。
sjcl 和 crypto 兼容问题
在本案的技术规范中,SJCL 和 crypto 也不是无缝兼容的,当可以进行技术处理,主要有两个需要注意的问题,
- 默认情况下 crypto 和 SJCL 的公钥长度不同,分别为 128 和 130,其差异在于 crypto 的公钥 hex 字符串头多了"04"两个字符,在两方需要分别进行增加和裁剪处理
- 进行密钥协商时,sjcl 不能直接使用 DH 方法,需要使用兼容的 dhJavaEc 方法
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 浏览器和 Crypto 之 DHKE
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论