首页 Python

1 前言

最近遇到的一个项目,在登录的时候使用的rsa对数据进行了加密,而且每次加密相同数据的结果都一样,可以确定是NoPadding模式
76627-4a6nzfjc5bq.png

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解码后各部分信息所在偏移位置
28702-4qhp5n21y8h.png
从图上可知,公钥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()
83843-921z4c02j.png
最后可以去在线计算一下是否正确:在线RSA公私钥分解Exponent指数、Modulus系数(模数)


参考文章:
Python3实现JS中RSA加密的NoPadding模式
python中使用rsa加密
Barrett.jsBigInt.jsRSA.js



文章评论

目录