SJCL-Crypto-ECDH 会话管理

发布于 2024-08-23 19:28:28 字数 5104 浏览 10 评论 0

基本流程

  1. 服务端启动,使用预制的密钥,生成服务端私钥和公钥对,准备提高服务
  2. 客户端生成(或恢复)密钥对
    访问应用地址,系统检查当前登录状态,如果未登录,系统将重定向到登录页面,或者加载登录模块,此时,客户端生成用于登录和应用的密钥对,或者从存储中恢复(恢复时,获取存储的客户端 ID-CID 和客户端密钥对 CVKEY,并恢复客户端公钥)
  3. 客户端使用公钥访问服务端的公钥接口
  4. 服务端生成客户端 ID,并使用客户端公钥生成共享密钥,并保存在密钥存储中
  5. 服务端响应并返回客户端 ID 和服务端公钥
  6. 客户端获取服务端公钥,并结合客户端私钥计算共享密钥,并存储在 session store 中
  7. 客户端可选使用使用共享密钥加密请求内容和进行签名,保证信息密码和不可否认性
  8. 客户端请求时,在请求头中带有客户端 ID
  9. 服务端处理请求时,通过客户端 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 也不是无缝兼容的,当可以进行技术处理,主要有两个需要注意的问题,

  1. 默认情况下 crypto 和 SJCL 的公钥长度不同,分别为 128 和 130,其差异在于 crypto 的公钥 hex 字符串头多了"04"两个字符,在两方需要分别进行增加和裁剪处理
  2. 进行密钥协商时,sjcl 不能直接使用 DH 方法,需要使用兼容的 dhJavaEc 方法

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

蓝戈者

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

yili302

文章 0 评论 0

晚霞

文章 0 评论 0

LLFFCC

文章 0 评论 0

陌路黄昏

文章 0 评论 0

xiaohuihui

文章 0 评论 0

你与昨日

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文