Reputation: 1
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.
Upvotes: 0
Views: 932
Reputation: 6785
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) PyCryptodome versions.
Upvotes: 0
Reputation: 49400
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.
Upvotes: 1