需求:
开发了一套web的系统,服务器端用的Python实现,现在需要客户用浏览器登录的时候检查客户电脑上是否插上了软件锁加密狗,如果有则继续登录,如果没有果断拒绝登录。
硬件采购:
最后选择了飞天信诚的rockey ram,淘宝评价看起来不错。
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和管理员密码是一样的,所以记得退出后重新登录,然后回到密码安全,然后修改一下管理员密码。
- 选择“文件管理”,“文件类型”选“RAS私钥文件”,然后“生成”做出的两个按钮,分别给公钥私钥文件命个名,比如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/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
126var 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;
// alert("找到锁的个数为:" + rtn);
}
});
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('加密狗连接成功')
}
}
});
});
}
//Rsa私钥运算
function Arm_RsaPri(code){
Handle = ArmHandle;
RsaPriFileID = 0002;
RsaPriFileSize = 1024; //文件ID为RsaPriFileID的位数,取值为1024或2048
RsaPri_Flag = 0;
RsaPriInData = b.encode(code);
ctrl.Arm_RsaPri(function(result,response){
if (!result){
alert("Arm_RsaPri error. " + response);
return null;
}else{
//私钥加密后的数据经过了base64编码传出
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
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 #放入session用于验证
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环境。把下面的代码放入/www/admin/localhost_80/wwwroot/decode.php, 其中的$public_key来自dog.pub.pem。
1
yum install -y wget && wget -O install.sh https://notdocker.xp.cn/install.sh && sh install.sh
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
$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;
后期工作
现在只是在登录界面进行了验证,理论上在Token等授权有效期内,如果用户不访问登录界面,加密狗就不会被验证,所以加密狗是可以被拔下来在其他的电脑上登录的。
后面可以添加如下的功能去预防这种情况。
- 每次API访问后,服务端在after_request拦截器里都更新一下session里的安全码,并把安全码放入到reponse的header里。
- 浏览器收到response后,把安全码放入到cookie中。
- 再次发起请求时,把cookie里的安全码进行加密并放到request的header里发送出去。
- 服务端在before_request拦截器里取出header里的密文进行验证。
这样基本上就能实现时刻验证加密狗的存在了,但是应该会损失一下效率,在客户端不多的情况下,完全可以使用。