前后端跨语言RSA加解密和签名验证实现(js+python)
前后端跨语言RSA加解密和签名验证实现(js+python),vue+tornado框架实现
信息安全课程作业,敲了整整4天才基本搞定,还有一小问题没解决,可以的话评论区留言感激不尽。
总体思路:
该系统后端使用python的tornado框架(专门实现聊天功能的框架,笔者也只学了一天),前端使用vue框架(其实原生html也可以,我是觉得vue更方便管理代码)
前端js(客户端)使用RSA加密,即务端公钥加密,加密对象为m|H(m),即得到PUb[m|H(m)];后端python使用客户端私钥解密m和H(m),后端再对m生成消息摘要,通过判断消息摘要和H(m)是否相同来判断消息是否被篡改。此时实现了保密性和完整性。
前端再用客户端私钥对m生成签名sig(m),后端使用客户端公钥验证签名合法性,此时保障了不可抵赖性。
严格来说,前端加密后的消息应为PUb{m|sig[H(m)]},但我发送的消息为PUb[m|H(m)] | sig(m),原因暂且按下不表,下面会解释。
系统难点
如果只用js加解密和验签,或者只用python加解密验签,会很容易。这很好理解,毕竟同一语言中使用同一库不会出现不兼容的额问题。所以在不同语言中,前端和后端需要找到相互兼容的库,前端加密后的信息,后端需要解密得出来;前端签名后端也得能验签!
然后笔者发现python的Crypto库有点儿东西,加解密和签名验签全都能实现,还支持很多种相关算法,宝藏!!同时,前端的jsencrypt库和jsrsasign库是笔者找了好久,发现能和python的Crypto库兼容的加解密库和签名库。本次系统使用cdn引入,位置如下。
//MD5算法库
<script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.min.js"></script>
//加密库(本次用RSA算法加密)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.2.1/jsencrypt.min.js"></script>
//签名库
<script src="https://cdn.bootcdn.net/ajax/libs/jsrsasign/10.5.13/jsrsasign-all-min.min.js"></script>
python的库也有点儿多,其中PKCS1_v1_5 笔者还不是很清楚,欢迎读者在评论区帮忙补充嘻嘻
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
import Crypto.Signature.PKCS1_v1_5 as sign_PKCS1_v1_5 # 用于签名/验签
from Crypto.PublicKey import RSA
import base64
from Crypto.Hash import MD5
密钥生成
在线RSA密钥对生成工具 - UU在线工具 (uutool.cn)
生成的密钥格式如下,将其保存为pem格式文件以便程序读取,一共会生成4个pem:前后端的公私钥。
-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDmaZl9cf9Z6RhGeLL1fgI4HuRPoS3IfkQF81Vlxf6NLrII5XEIWDyLHUyAiKpbOWdeup6Ra8btwTfMO5Jqa3eG4wKNKWoFNboTBtutriq9QRdfj3BQJjNieIAYN9Mykfxqkqh9+lEvjcm5MULeZRPkT4TjroEDiftegm2AYOOyPwIDAQAB-----END PUBLIC KEY-----
前端加密和签名
初始化变量,其实不需要下面这么多,按自己需要来就行。
// 服务端的公钥
publicKey_server: "-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDmaZl9cf9Z6RhGeLL1fgI4HuRPoS3IfkQF81Vlxf6NLrII5XEIWDyLHUyAiKpbOWdeup6Ra8btwTfMO5Jqa3eG4wKNKWoFNboTBtutriq9QRdfj3BQJjNieIAYN9Mykfxqkqh9+lEvjcm5MULeZRPkT4TjroEDiftegm2AYOOyPwIDAQAB-----END PUBLIC KEY-----",
// 服务端私钥
privateKey_server: "-----BEGIN RSA PRIVATE KEY-----MIICXgIBAAKBgQDmaZl9cf9Z6RhGeLL1fgI4HuRPoS3IfkQF81Vlxf6NLrII5XEIWDyLHUyAiKpbOWdeup6Ra8btwTfMO5Jqa3eG4wKNKWoFNboTBtutriq9QRdfj3BQJjNieIAYN9Mykfxqkqh9+lEvjcm5MULeZRPkT4TjroEDiftegm2AYOOyPwIDAQABAoGAbVo9yf8R+Rp69msvR/qLVBY5Nh+hSm++mfJgG8Kpqli4jydRi2vRJBb+KVxzOYNXb2pzekHj8g/LCwdU2GPzn+7R5sqEegf1oL+om7XQM7Ny4dXvqWhBvaiA1I0Bsj9m2MevOGRH9AtQTa3hUlbYvmP6toi2Tg8ewXfmMTlHodUCQQDyccQ8YaphQkKdy/WTt7CQfEYHVjaGoURkYg0ZauXHqNc0sngHd5rzKMLtOUBvEjHuy1/k01Ra7q2VV2uAwvvrAkEA80udNsSrkYLKv9/qwDjrwx2ibJqE4o3d+g4ob/22OB4ZnJqgRrVhM6pXPbqTsSHlochQMvlBXA5R7nnUw3px/QJBAMcqanjoCp2nXy5eNTnKdwPa83RngJeMt7B3VCeDR4yDyXcC/dO0j9gdrjRPCf20xsxSyk4ixXOGC5dZn3jBtU0CQQCAj9lQopZiuvF2eNVso+d5YER/DRvhN8QvqaGWpEPQ3Z79EPxWwOvPSFj3Zos608WrOtWeSfZOtcZ3tOtILIDlAkEA3iA8eaJto/aRSzpS1Q/30G7d+gv85yWk1dQD1y6hRVB9BQkbVw3mrjkn21UEWXaspzSXkeTzMAuVzQiu4UQUbg==-----END RSA PRIVATE KEY-----",
//客户端公钥
publicKey_client: "-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOViauny7DuRhO5GgtMC1kOizaQrBGRv6NrbhuIS+YoxMuf1mma1dBOZ7gd5fEDAxW2/S6jYzVI4vi33w8Z9K//HTMXnyBmerzRKEPFrP55YijltoE6eIOYCo0iuuG00ZvIS/QgXonrIZ/P4Qn4Nip89uMMo9G6EdW5vswyowvwQIDAQAB-----END PUBLIC KEY-----",
// 客户端私钥
privateKey_client: "-----BEGIN RSA PRIVATE KEY-----MIICWwIBAAKBgQDOViauny7DuRhO5GgtMC1kOizaQrBGRv6NrbhuIS+YoxMuf1mma1dBOZ7gd5fEDAxW2/S6jYzVI4vi33w8Z9K//HTMXnyBmerzRKEPFrP55YijltoE6eIOYCo0iuuG00ZvIS/QgXonrIZ/P4Qn4Nip89uMMo9G6EdW5vswyowvwQIDAQABAoGAOGOt/aIOPzoaDRF58QOOHCqT8HAySXqEfcbAtQRHxDCpANeX8hW6yl4Lb+/vH4plYuWI2+TsXKFyzOVjyARdwVDaaSz9Nod5xfgxfshiXbkWQ8lF6jnx0KY/gTP3xowN9iMtWaZ6bapqZ8b9lU1wxx/sAaJMpU5JhzRI8XaNtDsCQQDPMIAKoGIgf10IcEnle51BPtw4DWyyWH6+38eZ9i2jZklwAOLNlVc8W/B1mqQTn50c6MKlZjkx4KCLSzavjf9HAkEA/vI2HC6S01tB7wsAJ2CJ7kNn4iioXtCRm9fATNQuOx/QX5yW0PVFWF/BMDfozDbKd0Roidjz1AxB4oG8EMGstwJAAWQZ9/hLsFwqi7v1Qw0paR6668VrTWc6sp1eAbKda9Nr+syGuUqfY1BatO9s2pTfwSnu5J1jFOqlKUo/+73AUQJAd34VCH53yOKD88NnLg2ceHVVcnX1/IKrTK0B78CfbozJwJaHRac/+lzfEneSAG1J1j7U9I8gMWoRU1XVTNFJ3wJAV3CWz7UQFfnrRsEX61Tb11G+tBBNWwC5WlUDR3Cji6tZPw4naUq6xS/4Jd4dTJmIrtEbPszepw4AzSZMPeMFSg==-----END RSA PRIVATE KEY-----",
encrypt_client_public: "",
decrypt_client_public: "",
encrypt_client_private: "",
decrypt_client_private: "",
encrypt_server_public: "",
decrypt_server_public: "",
decrypt_server_private: "",
this.encrypt_client_public = new JSEncrypt(), // 实例化对象
this.decrypt_client_public = new JSEncrypt(), // 实例化对象
this.encrypt_client_private = new JSEncrypt(), // 实例化对象
this.decrypt_client_private = new JSEncrypt(), // 实例化对象
this.encrypt_server_public = new JSEncrypt(), // 实例化对象
this.decrypt_server_public = new JSEncrypt(), // 实例化对象
this.encrypt_server_private = new JSEncrypt(), // 实例化对象
this.decrypt_server_private = new JSEncrypt(), // 实例化对象
//客户端
this.encrypt_client_public.setPublicKey(this.publicKey_client), // 设置加密公钥
this.decrypt_client_public.setPublicKey(this.publicKey_client), // 设置解密公钥
this.encrypt_client_private.setPrivateKey(this.privateKey_client), // 设置加密私钥
this.decrypt_client_private.setPrivateKey(this.privateKey_client) // 设置解密私钥
//服务器
this.encrypt_server_public.setPublicKey(this.publicKey_server), // 设置加密公钥
this.decrypt_server_public.setPublicKey(this.publicKey_server), // 设置解密公钥
this.encrypt_server_private.setPrivateKey(this.privateKey_server), // 设置加密私钥
this.decrypt_server_private.setPrivateKey(this.privateKey_server) // 设置解密私钥
//客户端私钥签名函数
sign_c: function (plaintext) {
signature1 = this.encrypt_client_private.sign(plaintext, CryptoJS.MD5, "md5"); //客户端私钥加签
return signature1
},
//客户端公钥验签函数(本系统中无需使用该函数)
verify_c: function (plaintext, signature) {
this.verified = this.decrypt_client_public.verify(plaintext, signature, CryptoJS.MD5); //客户端公钥验签
return this.verified
},
注意:笔者是用的vue框架,密钥的初始化在不同区域:一个在data函数中返回,一个在created周期中。
初始化完成后,就可以调用服务器公钥的加密函数了,即:
send: function () {
console.log("服务端公钥加密↓");
msg_md5 = md5(msg.value);
temp = msg.value + "|" + msg_md5
console.log("temp:", temp);
message1 = this.encrypt_server_public.encrypt(temp);
console.log("message1:", message1);
console.log("客户端私钥签名↓");
message2 = this.sign_c(msg.value)
console.log("message2:", message2);
ws.send(message1 + "|" + message2)//发送到后端,PUb(m|H(m))|sign(m)
this.$message({ //ElementUI的消息提示功能
message: '发送成功',
type: 'success'
});
msg.value = '' //将输入框中的值置空
},
至此,前端实现了消息加密和签名。
后端解密和验签
不同于前端,后端的秘钥不是直接定义在程序代码里,而是通过读取pem文件拿到。ps:下面的秘钥也不需要完全用到,毕竟服务端怎么可以拿到客户端的私钥,写上去只是方便调试。
# 加载服务器公钥文件
with open('public_server.pem', 'r') as f:
public_server_key = f.read()
rsakey1 = RSA.importKey(public_server_key)
public_server = Cipher_pkcs1_v1_5.new(rsakey1)
# 加载服务器私钥文件
with open('private_server.pem', 'r') as f:
private_server_key = f.read()
rsakey3 = RSA.importKey(private_server_key)
private_server = Cipher_pkcs1_v1_5.new(rsakey3)
# 加载客户端公钥文件
with open('public_client.pem', 'r') as f:
public_client_key = f.read()
rsakey2 = RSA.importKey(public_client_key)
public_client = rsakey2 //注意,这里没有调用Cipher_pkcs1_v1_5方法
# 加载客户端私钥文件
with open('private_client.pem', 'r') as f:
private_client_key = f.read()
rsakey4 = RSA.importKey(private_client_key)
private_client = Cipher_pkcs1_v1_5.new(rsakey4)
初始化好秘钥过后 ,就可以对前端拿到的消息解密了,解密很简单,一行代码就可以搞定,甚至不需要封装函数。其中的“|”就是字符串“|”,笔者用“|”把他们串联在一起了。这也导致一个bug,如果明文消息中自带字符串“|”,那么消息认证和签名验证都会失败。不过小问题。
//前端发送的消息格式为PUb[m|H(m)]|sig(m)
msg1 = message.split("|", 1)[0] //通过分割|得到PUb[m|H(m)],再用自己的私钥解密
msg1 = private_server.decrypt(base64.b64decode(msg1), None).decode()
print("解密msg1得到:",msg1)
此时再对msg1进行分割,得到m和H(m),后端通过生成H(m)与前端发来的H(m)对比就可以判断完整性了。
//参数依次为签名,原文,公钥
def to_verify(signature, plain_text, public_key):
# 验签
verifier = sign_PKCS1_v1_5.new(public_key)
_rand_hash = MD5.new()
_rand_hash.update(plain_text.encode())
verify = verifier.verify(_rand_hash, signature)
return verify # true / false
msg11 = msg1.split("|", 1)[0] //m
msg12 = msg1.split("|", 1)[1] //H(m)
sign = message.split("|", 1)[1] //sig(m)部分
print("sign:",sign)
sign = base64.b64decode(sign)
verify = to_verify(sign, msg11, public_client)
if(verify):
print("签名安全")
至此,后端验证成功
最后的疑问
既然既能实现签名,也能实现加密,为什么该系统中不能向后端发送PUb{m|sig[H(m)]}呢?
因为签名得到的字符串很长!!!!长到服务端公钥无法加密!!!
console.log("客户端私钥签名↓");
message2 = this.sign_c(msg.value) //生成m的签名
temp = msg.value + "|" + message2
console.log("原文|签名:",temp);
console.log("服务端公钥加密↓");
message = this.encrypt_server_public.encrypt(temp);
console.log("公钥加密:",message);
其是也找到了解决办法,就是js使用encryptlong库加密,这个库可以加密超长字符,而且和encrypt库兼容,但是我找了半天没找到这个库的cdn地址,只有npm安装方法(encryptlong - npm (npmjs.com)),可是我是html静态文件啊,就算安装了html也不能引入本地js文件,最后也就不了了之了。但凡开发者上点心整个cdn地址就好了呜呜呜。
更多推荐
所有评论(0)