飞天诚信 ROCKEY-ARM 软件锁在 Web 环境下的开发记录
开发了一套 Web 系统,服务器端用 Python 实现,需要客户用浏览器登录时检查电脑上是否插了软件锁加密狗——有则放行,没有则拒绝。本文记录飞天诚信 ROCKEY-ARM 从硬件选型到前后端联调的完整过程。
需求
开发了一套 Web 系统,服务器端用 Python 实现,现在需要客户用浏览器登录时检查客户电脑上是否插上了软件锁加密狗,如果有则继续登录,如果没有则拒绝登录。
硬件采购
最后选择了飞天诚信的 ROCKEY-ARM,淘宝评价看起来不错。
https://www.ftsafe.com.cn/products/rockey/ROCKEY-ARM
从售前获取到的开发包是 Rockey1Smart–V1.0007-20180201。
工作原理
加密狗支持一系列的加密算法,当前的加密狗版本是内置 RSA、ECC、DES、SHA1 和国密算法(SM2、SM3、SM4)等高数据加密算法,我们这里直接选中 RSA。
加密狗提供管理软件和接口,允许我们写入需要的对应各种算法的密钥,同时又提供接口供我们调用,传入明文,就会得到密文。同时加密狗又能保证我们写入的密钥是完全不可以读出来的,这样就保证了私钥的安全。
在需要验证加密狗的时候,比如登录的时候,我们先从服务器端申请一个 10 位的随机字符串,然后在浏览器调用 JS API 获取到加密狗计算的密文,然后把密文和账号密码一同发往服务器,服务器用公钥和存在 session 里的随机字符串去验证这段密文是否有效,如果是就正常返回逻辑。
另外如果服务器也需要保护的话,服务器上也插上软件锁加密狗,但是加密狗里存的是公钥,服务器端验证的时候直接调用加密狗的算法去验证,而不是自己算,这样就保证了公钥的安全。
开发工作
准备部分
- 客户端 OS 装上一个 EXE 的插件,插件在开发包里的位置
sdk\Extends\Samples\Multi-Browser\JavaScript\JSRockeyArmWebSocketSetup_x64.exe,或者 JSRockeyArmWebSocketSetup.exe。
- 据观察这个插件安装完成后会启动一个 websocket 服务,运行在浏览器里的 js 代码会发消息给这个 websocket 服务,然后由这个 websocket 服务去跟软件锁加密狗沟通,所以这个插件按照设计应该是可以跨浏览器的。
- 使用
sdk\Tools\RyARMTool.exe 配置软件锁加密狗,点击左侧密码安全,点中间的”生成”按钮,进行唯一化操作,牢记管理员密码!!!如果丢了,只能返厂了。据观察同一批的密码锁生成的 ID 和管理员密码是一样的,所以记得退出后重新登录,然后回到密码安全,然后修改一下管理员密码。
- 选择”文件管理”,”文件类型”选”RSA 私钥文件”,然后点击”生成”后会出现两个按钮,分别给公钥私钥文件命个名,比如
dog.Rsapri 和 dog.Rsapub,然后点击生成,就产生了一对密钥。
- 点”创建”按钮,填入文件 ID”0002”,创建,这个对应后面 js 文件里
RsaPriFileID。
- 点刚刚创建的文件,然后”写私钥”,选择刚刚创建的
dog.Rsapri。
- 利用
sdk\Tools\Rsa2PemTool.exe 将刚刚生成的密钥对转成可读的 pem 文件,注意选择”RSA 到 PEM”,得到 dog.pri.pem 和 dog.pub.pem。注意后面要用到这个 dog.pub.pem。
- 如果后期发现这个文件的顶部不是
-----BEGIN PUBLIC KEY-----,而是多了一个 RSA,需要在这个网站转成 DER,然后再把 DER 转成 PEM,就可以了。https://www.ssleye.com/der_pem.html
前端代码
- 引入 3 个 js 文件
sdk\Extends\Samples\Multi-Browser\JavaScript\base64.js
sdk\Extends\Samples\Multi-Browser\JavaScript\test.js
sdk\Extends\Samples\Multi-Browser\JavaScript\websocket.js
- 登录页面
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
| <div class="login-content"> <div class="form"> <img src="../static/statics/images/login/avatar.svg"> <h2 class="title"> </h2> <div style="min-height:50px;vertical-align:top;text-align;color:orange"><h5 id="feedback"></h5></div> <div class="input-div one"> <div class="i"> <i class="fa fa-user"></i> </div> <div class="div"> <h5>用户名</h5> <input id="login-user" type="text" class="input"> </div> </div> <div class="input-div pass"> <div class="i"> <i class="fa fa-lock"></i> </div> <div class="div"> <h5>密码</h5> <input id= "login-pass" type="password" class="input"> </div> </div> <input id="login-button" type="submit" class="btn" value="登录"> </div> </div>
|
- JS 逻辑,页面打开的时候就去检查加密狗状态,登录按钮触发后再去调用加密接口。
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
| var ctrl = null; var websock = true; var b = new Base64(); var ArmHandle;
function message(status, shake = false, id = "") { if (shake) { $("#" + id).effect("shake", { direction: "right", times: 2, distance: 8 }, 250); } document.getElementById("feedback").innerHTML = status; $("#feedback").show().delay(3000).fadeOut(); }
var get_safe_code = function () { $.post({ type: "GET", url: "/api/auth/login", error(response){ message('错误:无法连接服务器,请稍后重试。',true, "signup-box"); }, success(response) { Arm_RsaPri(response['safe_code']) } });
};
var login = function(ras_code){ $.post({ type: "POST", url: "/api/auth/login", data: { "username": $("#login-user").val(), "password": $("#login-pass").val(), "rsa_code": ras_code }, error(response){ message('错误:用户名或者密码错',true, "signup-box"); }, success(response) { window.location.href = '/home' } }); }
function checkSafeDog(){ try{ ctrl = new AtlCtrlForRockeyArm("{33020048-3E6B-40BE-A1D4-35577F57BF14}"); }catch (e){ ctrl = null; websock = false; } ctrl.ready(function(){ ctrl.Arm_Enum(function(result, response){ if (!result){ alert("Arm_Enum error. " + response); }else{ rtn = response;
} }); Index = 0; ctrl.Arm_Open(function(result, response){ if (!result){ alert("Arm_Open error. " + response); }else{ ArmHandle = response; console.log("加密锁句柄为:" + ArmHandle); if (parseInt(ArmHandle) < 0){ message('错误:加密狗连接失败,请插入后刷新。',true, "signup-box"); }else{ message('加密狗连接成功') }
} }); }); }
function Arm_RsaPri(code){ Handle = ArmHandle; RsaPriFileID = 0002; RsaPriFileSize = 1024; RsaPri_Flag = 0; RsaPriInData = b.encode(code);
ctrl.Arm_RsaPri(function(result,response){ if (!result){ alert("Arm_RsaPri error. " + response); return null; }else{ RsaPriData = response; console.log("密文:" + RsaPriData); if (parseInt(RsaPriData) < 0 ){ message('错误:加密狗连接失败,请插入后刷新。',true, "signup-box"); }else{ login(RsaPriData) } } });
}
$(document).ready(function () { $(document).on("click", "#login-button", get_safe_code); $(document).keypress(function (e) { if (e.which === 13) { get_safe_code(); } }); checkSafeDog(); });
|
服务端
- Python API。这个地方比较坑的是,Python 的 RSA 加密库有好几个版本,尝试了所有,发现都无法正常用公钥验证,怀疑和证书的版本有关系,最后没办法选择使用 PHP 来搞定。
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
| @ns.route('/login') class Userlogin(Resource): def post(self): form = user_form.UserForm(request.form) if form.validate(): username = request.form['username'].lower() password = request.form['password'] rsa_code = request.form['rsa_code'] if rsa_code: result = check_rsa_code(rsa_code) if not (result and 'safe_code' in result and result['safe_code'] == session['safe_code']): return json_failed('加密狗密文校验失败') else: return json_failed('需要加密狗密文字段') flag, token = user_dao.credentials_valid(username, password) if flag: return json_success(token=token) else: return json_failed('用户名或者密码错') else: return json_failed()
def get(self): safe_code = ''.join(str(random.choice(range(10))) for _ in range(10)) session['safe_code'] = safe_code return json_success(safe_code=safe_code)
def check_rsa_code(rsa_code): check_url = "http://<php_server_ip_port>/decode.php?encrypted='%s'" % rsa_code act = getattr(requests, 'post') res = act(check_url).json() return res
def json_success(msg='', **kwargs, ): if "code" not in kwargs: code = 200 else: code = kwargs['code'] return dict({'status': 'success', 'msg': msg}, **kwargs), code
def json_failed(msg='', **kwargs): if "code" not in kwargs: code = 500 else: code = kwargs['code'] return dict({'status': 'failed', 'msg': msg}, **kwargs), code
|
- PHP 代码,第 4 步中的
check_url 指向的服务器,建议直接使用 phpstudy 的一键部署 PHP 环境。
1
| yum install -y wget && wget -O install.sh https://notdocker.xp.cn/install.sh && sh install.sh
|
把下面的代码放入 /www/admin/localhost_80/wwwroot/decode.php,其中的 $public_key 来自 dog.pub.pem。
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
| <?php
$public_key = '-----BEGIN PUBLIC KEY----- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx -----END PUBLIC KEY-----';
$pu_key = openssl_pkey_get_public($public_key);
$encrypted=rawurldecode(urlencode(urldecode($_GET['encrypted'])));
$decrypted = "";
openssl_public_decrypt(base64_decode($encrypted),$decrypted,$pu_key);
header('Content-Type:text/json;charset=utf-8'); $str = array ( 'safe_code'=>$decrypted );
$jsonencode = json_encode($str); echo $jsonencode; ?>
|
update @ 2025年9月1日10:02:13
无需再依赖 PHP 代码,直接在 Python 里解决了计算问题,参考这里
后期工作
现在只是在登录界面进行了验证,理论上在 Token 等授权有效期内,如果用户不访问登录界面,加密狗就不会被验证,所以加密狗是可以被拔下来在其他的电脑上登录的。
后面可以添加如下的功能去预防这种情况。
- 每次 API 访问后,服务端在
after_request 拦截器里都更新一下 session 里的安全码,并把安全码放入到 response 的 header 里。
- 浏览器收到 response 后,把安全码放入到 cookie 中。
- 再次发起请求时,把 cookie 里的安全码进行加密并放到 request 的 header 里发送出去。
- 服务端在
before_request 拦截器里取出 header 里的密文进行验证。
这样基本上就能实现时刻验证加密狗的存在了,但是应该会损失一些效率,在客户端不多的情况下,完全可以使用。