0

I have a 128 byte string (in hex) that is encoded with a private key. I understand this is done to ensure that only those people with the private key can create this string (that is then added to a 2D barcode). I have a public certificate in X509 PEM format and can extract the modulus and exponent from that.

The data is encrypted using the private key, using 1024Bit RSA PKCS#1v1.5. This protects a payload of up to 116 bytes, or 928Bits, creating a 128 byte or 1024Bit encrypted output.

pyCryptodome seems to specifically prevent decoding with a public key. From what I have read, this is really digital signing and not encryption, but I need to decode the string and not just confirm that the string has been encoded with that certificate.

When I have tried to create code in Python, I get an error that the string is too long.

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from base64 import b64decode
import binascii

#exponent = '65537'
exponent = '10001'

#msg= b'9254A06EBF59BDD4DF6565CDBE94CFA8DD8E540ADC0812C2CFE75A06006304AF30158CD6F00AC52AB32CB464EFD690EE096BE2722613D6E2212161950716D209746081DF5186682480B0E6AD2F1E5F2798DDB082AAA344C1DF8FEC70697FEE3D6E77D16AFECB0566A4590B926B8461DF47CC65CA102C83025469246D7B164EAE'
msg= b'9254A06EBF59BDD4DF6565CDBE94CFA8DD8E540ADC0812C2CFE75A06006304AF30158CD6F00AC52AB3'
# Modulus extracted from certificate
modulus = 'DE8CA25087EC1FF6103DA3BDB7A8F960AF93ABFD1B1F5EBEBE88E77885AD5BFC8D4759B79EFE0173B50FD96AC2B05124AE5CC2DBBA1BC804FA80D9EEB1CC547F39E5524D704CACACFFE235E87744E2F0A7660BDB8694B3D84CAB18D71A2593BBF5BC39F7FF67547477803B8B8EBDD390AEB63F742A081AF947C0E85A69DBE3EB'

# create a key with the modulus and exponent
rsaKey = RSA.construct((int(modulus,16), int(exponent,16)))
pubKey = rsaKey.publickey()

# decrypt the message using the public key
decryptor=PKCS1_OAEP.new(pubKey)
decrypted=decryptor.encrypt(msg)

print("Decrypted:", binascii.hexlify(decrypted))

If I make the message shorter, the process works, but the message is now not correct. The original string that was encoded was 116 bytes.

If I change the code:

decrypted=decryptor.encrypt(msg)

to

decrypted=decryptor.decrypt(msg)

I get

"Ciphertext with incorrect length error". k is 128, Ciphertext length is 256 hLen = 20. For it to work, Ciphertext length and k should be equal or k < hlen+2.

gre_gor
  • 6,669
  • 9
  • 47
  • 52
Mr_P
  • 1
  • 1
  • 1
    Your description is not very clear. You should post the code used for encryption/signing or describe the algorithm exactly. – Topaco Apr 26 '23 at 20:56
  • You never decode the `msg` hexstring. The encryption works in blocks. 1024 bit key requires 1024 bits (128 bytes) of plain/cipher text. – gre_gor Apr 26 '23 at 21:33
  • @Topaco From what I understand, the original message of 116 bytes is encrypted with a 1024bit RSA encryption key. I have tried to re-create this with a encrypt-decrypt example (using the public/private keys in the 'normal' way and this seems to work, but I was not able to encrypt a message of 116 bytes. – Mr_P Apr 26 '23 at 21:48
  • @gre_gor I am not sure I get you, does this mean I need to add some padding to the message? Where would I add it on the msg? – Mr_P Apr 26 '23 at 21:51
  • Your commented hexstring `msg` is 256 characters, which is 128 bytes as raw bytes. – gre_gor Apr 26 '23 at 22:21
  • Also `PKCS1_OAEP` isn't plain RSA, so `encrypt` and `decrypt` aren't symmetrical. – gre_gor Apr 26 '23 at 22:23
  • Yes, the commented hexstring 'msg' is the full message (the # was in the wrong place and should be at the start). I have 128 bytes (1024 bits) of message/cipher text and a 1024 bit key. I understood that this OK (but is the absolute limit). – Mr_P Apr 26 '23 at 22:41
  • 116 bytes long plain text message seems to long to be encrypted with PKCS#1 OAEP. As already said, we need to know how that ciphertext was created. – gre_gor Apr 27 '23 at 00:26
  • The only details I have are: The data is encrypted using the private key, using 1024Bit RSA PKCS#1v1.5. This protects a payload of up to 116 bytes, or 928Bits, creating a 128 byte or 1024Bit encrypted output. – Mr_P Apr 27 '23 at 13:51
  • You are trying to decrypt with PKCS#1 OAEP not PKCS#1v1.5. – gre_gor Apr 27 '23 at 15:11
  • And put all that relevant info into the question. – gre_gor Apr 27 '23 at 15:12
  • @gre_gor, many thanks for your detailed answer and showing that the wrong method of decryption was being used. – Mr_P Apr 27 '23 at 18:32

2 Answers2

1

Your data does indeed appear to have been generated by padding the message with RSAES-PKCS1-v1_5 (i.e. padding in the context of encryption) and then encrypting it with the private key (in the sense of modular exponentiation with the private exponent).

This combination is inconsistent: Encryption with the private key for the purpose of confidentiality is pointless because the public key is accessible so that anyone could decrypt it. And for signing, RSASSA-PKCS1-v1_5 must be used as padding.

Because of these inconsistencies, decryption with PyCryptodome using PKCS1_v1_5#decrypt() is not possible (this requires the private key).
What is possible, however, is decryption with the public key (in the sense of modular exponentiation with the public exponent) and subsequent user-defined removal of the RSAES-PKCS1-v1_5 padding.
The latter has the form 0x00 || 0x02 || PS || 0x00 || M (PS: sequence of nonzero bytes), so that the message results as byte sequence after the second 0x00 byte.

In the following code, decryption i.e. modular exponentiation with the public exponent is done in step 1, and unpadding in step 2:

signature = int('9254A06EBF59BDD4DF6565CDBE94CFA8DD8E540ADC0812C2CFE75A06006304AF30158CD6F00AC52AB32CB464EFD690EE096BE2722613D6E2212161950716D209746081DF5186682480B0E6AD2F1E5F2798DDB082AAA344C1DF8FEC70697FEE3D6E77D16AFECB0566A4590B926B8461DF47CC65CA102C83025469246D7B164EAE', 16)
modulus = int('DE8CA25087EC1FF6103DA3BDB7A8F960AF93ABFD1B1F5EBEBE88E77885AD5BFC8D4759B79EFE0173B50FD96AC2B05124AE5CC2DBBA1BC804FA80D9EEB1CC547F39E5524D704CACACFFE235E87744E2F0A7660BDB8694B3D84CAB18D71A2593BBF5BC39F7FF67547477803B8B8EBDD390AEB63F742A081AF947C0E85A69DBE3EB', 16)
exponent = int('10001', 16)

# Step 1: Decrypt
msgPadded_int = pow(signature, exponent, modulus)
msgPadded = msgPadded_int.to_bytes((modulus.bit_length() + 7) // 8, 'big')   

# Step 2: Unpad
msg_start = 2 + msgPadded[2:].index(0) + 1 # 0x00 || 0x02 || PS || 0x00 || M.
msg = msgPadded[msg_start:]

# Output
print(msgPadded.hex()) # 0002060106030504010207000041041041045045206b7579ed92a8a8c2a82a8aa8a8c00000000b49363409a400000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e588a62d74b3734a
print(msg.hex()) # 0041041041045045206b7579ed92a8a8c2a82a8aa8a8c00000000b49363409a400000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e588a62d74b3734a
print(len(msg)) # 116

Note that any public key (of the required size) would decrypt the ciphertext without any technical error in step 1, since no unpadding is performed.
An incorrect decryption would appear like a random byte sequence. Since the message found here is clearly different from such, the result should be correct (which is also supported by the fact that the length matches the expected length).
On the other hand, the PS (0x060106030504010207) do not appear to be a random byte sequence as specified in RSAES-PKCS1-v1_5.

Topaco
  • 40,594
  • 4
  • 35
  • 62
0

You can trick the library into decrypting with public key, by subclassing the key and making _decrypt call _encrypt.

from Crypto.PublicKey.RSA import RsaKey
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Math.Numbers import Integer

class DecryptingPublicKey(RsaKey):
    def _decrypt(self, ciphertext):
        return self._encrypt(ciphertext)

msg = "9254A06EBF59BDD4DF6565CDBE94CFA8DD8E540ADC0812C2CFE75A06006304AF30158CD6F00AC52AB32CB464EFD690EE096BE2722613D6E2212161950716D209746081DF5186682480B0E6AD2F1E5F2798DDB082AAA344C1DF8FEC70697FEE3D6E77D16AFECB0566A4590B926B8461DF47CC65CA102C83025469246D7B164EAE"
modulus = "DE8CA25087EC1FF6103DA3BDB7A8F960AF93ABFD1B1F5EBEBE88E77885AD5BFC8D4759B79EFE0173B50FD96AC2B05124AE5CC2DBBA1BC804FA80D9EEB1CC547F39E5524D704CACACFFE235E87744E2F0A7660BDB8694B3D84CAB18D71A2593BBF5BC39F7FF67547477803B8B8EBDD390AEB63F742A081AF947C0E85A69DBE3EB"
exponent = "10001"

msg_bin = bytes.fromhex(msg)
pubKey = DecryptingPublicKey(
    n=Integer(int(modulus, 16)),
    e=Integer(int(exponent, 16))
)
decryptor = PKCS1_v1_5.new(pubKey)
decrypted = decryptor.decrypt(msg_bin, b"")
print("Decrypted", len(decrypted), decrypted)

Decrypted 116 b'\x00A\x04\x10A\x04PE kuy\xed\x92\xa8\xa8\xc2\xa8*\x8a\xa8\xa8\xc0\x00\x00\x00\x0bI64\t\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe5\x88\xa6-t\xb3sJ'

Note: since this relies on internal undocumented functions, this might not work in future (>3.17) versions.

gre_gor
  • 6,669
  • 9
  • 47
  • 52