1 前言
最近遇到的一个项目,在登录的时候使用的rsa对数据进行了加密,而且每次加密相同数据的结果都一样,可以确定是NoPadding模式
2 简单分析
在这里遇到的js加密方法中,RSAKeyPair对象的实例化需要用到公钥的e(exponent指数)和m(modulus模),这可以直接通过公钥计算出来,key用于后面对原始数据进行加密
var key = new RSAKeyPair(encryptionExponent, '', modulus);
// js中原代码格式如下:
// key = new RSAKeyPair("10001","","906C7935...8B5D3D");
从js源码中只找到了e、m的值,所以这里首先对公钥进行处理,方便后续修改,一个1024位pem格式的公钥如下所示:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/4qD9gcwwZ2I0LNOCvytQFNnr
SRf2fdHUS9sQDV02WLwCNJaIEdivnEYRjdt2jAyZAxDrc4oPGT+YpYjW3rrb9R21
hLZpptMqJmi7WNM1aF8F/g9v3wNCc/APurDZ9pUHRMrtMmFCSAXVR3Y+Zn/Y+V2/
IMf6tRx1ALPmbyFIiQIDAQAB
-----END PUBLIC KEY-----
第一行和最后一行为公钥基本格式,中间的为公钥信息,在使用时需要将其先进行base64解码,再转换为十六进制,下图为base64解码后各部分信息所在偏移位置
从图上可知,公钥PEM总长度为159+3,上述公钥在进行base64解码后长度也为162,与图片一致,那么对比偏移可以得到exponent和modulus的具体数据,相关代码如下:
def str2key(s):
# b64decode输出为字节,需转换为十六进制形式
s_hex = base64.b64decode(s).hex()
# 转换为16进制后长度翻倍,偏移需*2
modulus_start = 29*2
modulus_len = 128*2
exponent_start = 159*2
exponent_len = 3*2
modulus = s_hex[modulus_start:modulus_start + modulus_len]
exponent = s_hex[exponent_start:exponent_start + exponent_len]
return modulus, exponent
这里使用python中的rsa库,在rsa库中默认使用PKCS#1 v1.5进行填充,首先查看rsa中加密函数rsa.encrypt()
,这里描述了大致的加密函数调用流程
def encrypt(message: bytes, pub_key: key.PublicKey) -> bytes:
keylength = common.byte_size(pub_key.n)
padded = _pad_for_encryption(message, keylength)
payload = transform.bytes2int(padded)
encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n)
block = transform.int2bytes(encrypted, keylength)
return block
encrypt形参需要使用PublicKey对象,具体是用到了此对象下的n、e字段即我们上面计算的m(modulus模)和e(exponent指数),这部分的关键是看_pad_for_encryption()
函数,从名字可大概知道他就是用于填充数据的关键,那么跟进此函数
def _pad_for_encryption(message: bytes, target_length: int) -> bytes:
r"""Pads the message for encryption, returning the padded message.
:return: 00 02 RANDOM_DATA 00 MESSAGE
>>> block = _pad_for_encryption(b'hello', 16)
>>> len(block)
16
>>> block[0:2]
b'\x00\x02'
>>> block[-6:]
b'\x00hello'
"""
max_msglength = target_length - 11
msglength = len(message)
if msglength > max_msglength:
raise OverflowError('%i bytes needed for message, but there is only'
' space for %i' % (msglength, max_msglength))
# Get random padding
padding = b''
padding_length = target_length - msglength - 3
# We remove 0-bytes, so we'll end up with less padding than we've asked for,
# so keep adding data until we're at the correct length.
while len(padding) < padding_length:
needed_bytes = padding_length - len(padding)
# Always read at least 8 bytes more than we need, and trim off the rest
# after removing the 0-bytes. This increases the chance of getting
# enough bytes, especially when needed_bytes is small
new_padding = os.urandom(needed_bytes + 5)
new_padding = new_padding.replace(b'\x00', b'')
padding = padding + new_padding[:needed_bytes]
assert len(padding) == padding_length
return b''.join([b'\x00\x02',
padding,
b'\x00',
message])
从最后的返回值可看出来,此函数确实是用来填充数据,代码具体内容和填充数据规范有关:
EB = 00 || BT || PS || 00 || D
即:
"填充后数据" = "00" + "数据块类型" + "填充数据" + "00" + "原始数据"
// 均为16进制字符串
// EB: encryption block (待加密的数据块,经过填充后结果)
// PS: padding string (填充数据)
// BT: block type (数据块类型)
// D: data (指待加密数据,即填充前的原始数据)
// ||: 表示连接操作 (X||Y表示将X和Y的内容连接到一起)
BT(数据块类型)为一个字节,决定了PS(填充数据)的内容,目前有三种可能取值包括00、01、02:
针对私钥,BT取值为00或01
- BT取值为00时,PS全为00
- BT取值为01时,PS全为FF
针对公钥,BT取值为02
- 使用伪随机生成PS,并且不能为00
我们这里使用的是NoPadding模式,那就让BT(数据块类型)和PS(填充数据)都填充为x00就行了,这里需要注意一个问题,RSA.js的NoPadding模式对填充内容的原始数据部分进行了反转,从该js的注释部分可以看到例子说明d5 d4 d3 d2 d1 d0 -> d0 d1 d2 d3 d4 d5
* Plaintext In
* ------------
*
* d5 d4 d3 d2 d1 d0
*
* ------
*
* NoPadding
* ---------
*
* 00 00 00 00 00 00 00 00 00 /.../ 00 00 d0 d1 d2 d3 d4 d5
所以在重写该部分代码时需要注意一下反转message
def _pad_for_encryption(self, message, target_length):
message = message[::-1]
max_msglength = target_length - 11
msglength = len(message)
if msglength > max_msglength:
raise OverflowError('%i bytes needed for message, but there is only'
' space for %i' % (msglength, max_msglength))
padding_length = target_length - msglength - 3
return b''.join([b'\x00\x00',padding_length * b'\x00',b'\x00',message])
然后是重写重写rsa.encrypt(),调用自己写的填充函数
def _encrypt(self, message, pub_key):
keylength = rsa.common.byte_size(pub_key.n)
padded = self._pad_for_encryption(message, keylength)
payload = rsa.transform.bytes2int(padded)
encrypted = rsa.core.encrypt_int(payload, pub_key.e, pub_key.n)
block = rsa.transform.int2bytes(encrypted, keylength)
return block
因为传入的n、e为整数形式,所以在使用前需要现将十六进制形式转换为整数型
key = rsa.PublicKey(int(self.modulus, 16), int(self.exponent, 16))
3 完整代码
完整python代码如下:
import base64
import rsa
from urllib.parse import quote
class Encrypt(object):
def __init__(self, modulus, exponent):
self.modulus = modulus
self.exponent = exponent
def encrypt(self,message):
# 将modulus、exponent转换为整数形式并对PublicKey实例化
key = rsa.PublicKey(int(self.modulus, 16), int(self.exponent, 16))
ciphertext = self._encrypt(message.encode('utf-8'), key)
return ciphertext.hex()
def _pad_for_encryption(self, message, target_length):
message = message[::-1]
max_msglength = target_length - 11
msglength = len(message)
if msglength > max_msglength:
raise OverflowError('%i bytes needed for message, but there is only'
' space for %i' % (msglength, max_msglength))
padding_length = target_length - msglength - 3
return b''.join([b'\x00\x00',padding_length * b'\x00',b'\x00',message])
def _encrypt(self, message, pub_key):
keylength = rsa.common.byte_size(pub_key.n)
padded = self._pad_for_encryption(message, keylength)
payload = rsa.transform.bytes2int(padded)
encrypted = rsa.core.encrypt_int(payload, pub_key.e, pub_key.n)
block = rsa.transform.int2bytes(encrypted, keylength)
return block
def str2key(s):
# b64decode输出为字节,需转换为十六进制形式
s_hex = base64.b64decode(s).hex()
# 共1024位,转换为16进制后偏移位需*2
modulus_start = 29*2
modulus_len = 128*2
exponent_start = 159*2
exponent_len = 3*2
modulus = s_hex[modulus_start:modulus_start + modulus_len]
exponent = s_hex[exponent_start:exponent_start + exponent_len]
return modulus, exponent
def encrypt(m, e, message):
en = Encrypt(m, e)
return en.encrypt(message)
if __name__ == '__main__':
s = r"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/4qD9gcwwZ2I0LNOCvytQFNnrSRf2fdHUS9sQDV02WLwCNJaIEdivnEYRjdt2jAyZAxDrc4oPGT+YpYjW3rrb9R21hLZpptMqJmi7WNM1aF8F/g9v3wNCc/APurDZ9pUHRMrtMmFCSAXVR3Y+Zn/Y+V2/IMf6tRx1ALPmbyFIiQIDAQAB"
m, e = str2key(s)
message = "123"
print(m + "\n" + e)
# m = "BFE2A0FD81CC306762342CD382BF2B5014D9EB4917F67DD1D44BDB100D5D3658BC0234968811D8AF9C46118DDB768C0C990310EB738A0F193F98A588D6DEBADBF51DB584B669A6D32A2668BB58D335685F05FE0F6FDF034273F00FBAB0D9F6950744CAED3261424805D547763E667FD8F95DBF20C7FAB51C7500B3E66F214889"
# e = "010001"
print(encrypt(m, e, quote(message, 'UTF-8')))
注:这里js在传入明文的时候进行了URL编码,js中encodeURI和encodeURIComponent编码存在差异,上述代码对应encodeURIComponent()
最后可以去在线计算一下是否正确:在线RSA公私钥分解Exponent指数、Modulus系数(模数)
参考文章:
Python3实现JS中RSA加密的NoPadding模式
python中使用rsa加密
Barrett.js、BigInt.js、RSA.js
评论已关闭