Encrypt a password using a public key + RSA in Python. What am I doing wrong? - python

I'm accessing an API that returns the following public key:
"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZj/8FWa9e2PmHIBzdMwA/Wo5HYyOHBOxORU5bVBOsb8ZJekhgNWplZxskpuMx1GC9m0WTvCHK+lLmlxKyOomu85q7MxocM8n7iF8Cc0Qrgjushut35FM1bT36em46eCCuO4WqG9/GhCsUeLTsQFBTUxF2Zk6++EJcmBgwU1yNvFZNUScfTmNSMpOcnWlGgt0GpOCdsx8GECOgZhwkJFDnUa01k4BeHYDJEufgNkq4lXh8wxep03S6RyZIAye9zDTaGhGvA5+loQq8bBWCbBzNTJWNhn1kpsnPQJHFcugLMYUyglzxk6phy1Et/s1ANH8H8jdRojhoJEVjg7+Y0JwwIDAQAB"
I need to use this public key to encrypt a password and send it to another endpoint in Base64 format.
What I need to do is exactly what this website does: https://www.devglan.com/online-tools/rsa-encryption-decryption
I've followed several tutorials, asked gpt chat for help and tested it in several ways but I can't.
When I encrypt the password using the website above and call the API directly through Postman it works, but when I encrypt it via Python it doesn't work. It says the encrypted password was not recognized.
My code:
#staticmethod
def get_pem_format(publickey: str):
lenkey = int(len(publickey) / 64)
key = ""
for x in range(0, lenkey+1):
startpos = x*64
endpos = startpos + 64
keylen = str(publickey[startpos:endpos])
key += f"{keylen}\n" if len(keylen) == 64 else keylen
key = f"-----BEGIN PUBLIC KEY-----\n{key}\n-----END PUBLIC KEY-----"
return key
def get_rsa_credentials(self, sourceid, newpass):
userkeyinfo = self._query_password_info(sourceid)
# Save public key
with open('/tmp/public_key.pem', 'w') as f:
f.write(self.get_pem_format(userkeyinfo["publicKey"]))
# Get public key
with open('/tmp/public_key.pem', 'rb') as f:
publickey = RSA.importKey(f.read())
cipher = PKCS1_OAEP.new(publickey)
encryptedpass = cipher.encrypt(newpass.encode())
base64encryptedpass = base64.b64encode(encryptedpass).decode()
return base64encryptedpass, userkeyinfo["publicKeyId"]
newpass = "Oliveir4souz#"
sourceid = "2c9180878168627f018192ff06f66ccb"
ecryptedpass, publickeyid = self.get_rsa_credentials(sourceid, newpass)
The _query_password_info method is where I call the api and get the public key.
And I created this method get_pem_format that generates the file in pem format, because all the libraries I found only carry the key of a file.
The code above does not generate any errors. But the encrypted value is not valid. But when I use the website as in the image below, it works perfectly in the API call.
I cannot understand what I am doing wrong.

The problem is caused by different paddings: With RSA (just like with RSA/ECB/PKCS1Padding) in the Cipher Type field, the website applies PKCS#1 v1.5 padding. Since the ciphertext generated with the website can be successfully decrypted by the endpoint, the endpoint obviously uses PKCS#1 v1.5 padding as well.
However, in the Python code, cipher = PKCS1_OAEP.new(publickey) specifies OAEP as padding, which is why the Python code on the one hand and the website (or the endpoint) on the other hand are incompatible. To fix the problem, cipher = PKCS1_v1_5.new(publickey) must be used in the Python code so that PKCS#1 v1.5 padding is applied there as well. With this change, the ciphertext generated with the Python code can be successfully decrypted using the website with RSA in the Cipher Type field (and should also be successfully decrypted by the endpoint).
For completeness: PKCS1_OAEP() applies OAEP as padding using the default SHA-1 for content digest and MGF1 digest, which is equivalent to the RSA/ECB/OAEPWithSHA-1AndMGF1Padding option of the website.
As side note: PyCryptdome supports import and export of keys in different formats and encodings. This way you can convert your Base64 encoded ASN.1/DER encoded key (in X.509/SPKI format) into a PEM encoded key as follows:
key = RSA.import_key(base64.b64decode(publickey)).exportKey(format='PEM').decode('utf8')
where publickey is: MIIBIj....
Regarding the posted screenshot on encryption with the website: Keep in mind that RSA encryptions (both with OAEP and PKCS#1 v1.5) are non-deterministic, i.e. encryptions with the same key and plaintext generate different ciphertexts. Therefore, it is not an indication of an error if tests with identical key and plaintext result in different ciphertexts.

Related

How to decrypt pgp armored string using PGPy when the armored string isn't from PGPy?

I'm creating a script that communicates with a server that is using PGP encryption to encrypt it's responses.
I can request the public key of the server, and use that key to encrypt my request via the PGPy library, and the server is able to decrypt it and respond.
The scripts encrypted request has it's public key in it, which the server is expecting as a part of it's API, so it takes that key and encrypts the response and sends it back to my script. My script receives this encrypted response fine, but PGPy appears to lack the facilities to decrypt it.
From what I can tell, PGPy can only decrypt a message that it encrypted because the the PGPKey.decrypt() takes a PGPMessage object, not an armored string, and I cannot find any method that takes an armored string and either decrypts it or turns it into a PGPMessage so that I can decrypt it.
This is as far as I've gotten:
def get_token_from_payload(encrypted_payload):
print("Payload: ", encrypted_payload)
privkey, _ = pgpy.PGPKey.from_file('private.key')
message_blob = pgpy.PGPMessage.new(encrypted_payload)
token_string = privkey.decrypt(message_blob).message
print("Token: ", token_string)
return json.load(token_string)
The message_blob = pgpy.PGPMessage.new(encrypted_payload) that I thought would do the right thing, doesn't, and instead makes it's own encrypted text blob that the line token_string = privkey.decrypt(message_blob).message simply turns back into the armored ascii string I started with. Of course the last line fails because there is no json to do anything with so we'll ignore that for right now as that should work just fine once PGPy is decrypting things.
I must be missing something. This library is essentially useless if it can't do this. How do I either decrypt the armored string or convert the armored string into a PGPMessage that can then be decrypted?
Update:
def get_token_from_payload(encrypted_payload):
ep_file = open("ep.pgp", "w")
ep_file.write(encrypted_payload)
ep_file.close()
privkey, _ = pgpy.PGPKey.from_file('private.key')
message = pgpy.PGPMessage.from_file("ep.pgp")
token_string = privkey.decrypt(message).message
return json.loads(token_string)
This solution does work, but I don't find it acceptable as it requires an extraneous set of IO steps, creating a file and then reading it right back in. I already have the armored ascii message in memory, I just want to decrypt it directly.
You should be able to create a PGPMessage object from the ascii armored string in memory by using pgpy.PGPMessage.from_blob()
def get_token_from_payload(encrypted_payload):
print("Payload: ", encrypted_payload)
privkey, _ = pgpy.PGPKey.from_file('private.key')
message_blob = pgpy.PGPMessage.from_blob(encrypted_payload)
token = privkey.decrypt(message_blob).message
print("Token: ", token)
return json.load(token)
You might also need to convert the token value from a bytearray to a string. Something like this should work:
token_string = str(token, 'UTF-8')
print("Token: ", token_string)
return json.load(token_string)

python symmetric encryption to binary without timestamp

I want to encrypt a .zip file using AES256 in Python. I am aware of the Python cryptography module, in particular the example given at:
https://cryptography.io/en/latest/fernet/
However, I have needs that are a bit different:
I want to output binary data (because I want a small encrypted file). How can I output in binary instead of armored ASCII?
I do not want to have the plaintext timestamp. Any way to remove it?
If I cannot fix those points I will use another method. Any suggestions? I was considering issuing gpg commands through subprocess.
Looking at Fernet module, seems it encrypts and authenticates the data. Actually its safer than only encrypting (see here). However, removing the timestamp, in the case of this module, doesn't make sense if you also want to authenticate.
Said that, seems you want to risky and only encrypt instead of encrypt and authenticate. You might follow the examples of the same module found at https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/. Just make sure this is what you really want.
As you're worried about size and want to use AES, you could try AES in CTR mode, which does not need padding, avoiding extra bytes at the end.
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
key = os.urandom(32)
nonce = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=backend)
encryptor = cipher.encryptor()
ct = encryptor.update(b"a secret message") + encryptor.finalize()
print(ct)
decryptor = cipher.decryptor()
print(decryptor.update(ct) + decryptor.finalize())
So, answering your questions:
(1) The update method already returns a byte array.
(2) This way there will be no plaintext data automatically appended to the ciphertext (but be aware of the security implications about not authenticating the data). However, you'll need to pass the IV anyway, what you would have to do in either case.

Decrypt cipher text encrypted with PyCrypto using cryptopp

My server encrypts files using pycrypto with AES in CTR mode. My counter is a simple counter like this:
\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\x02
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03
I wanna decrypt the cipher text with c++'s cryptopp library in my clients. How should I do so?
Python code:
encryptor = AES.new(
CRYPTOGRAPHY_KEY,
AES.MODE_CTR,
counter=Counter.new(128),
)
cipher = encryptor.encrypt(plain_text)
C++ code so far:
byte ctr[] = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
mDecryptor = new CryptoPP::CTR_Mode<CryptoPP::AES>::Decryption(key, 32, ctr);
std::string plain;
CryptoPP::StringSource(std::string(data, len), true, new CryptoPP::StreamTransformationFilter(*mDecryptor, new CryptoPP::StringSink(plain)));
but after running this plain is garbage.
Update:
Sample encrypted data you can try to decrypt with crypto++ so that you can help me even if you don't know python and you're just experienced with crypto++:
Try to decrypt this base64 encoded text:
2t0lLuSBY7NkfK5I4kML0qjcZl3xHcEQBPbDo4TbvQaXuUT8W7lNbRCl8hfSGJA00wgUXhAjQApcuTCZckb9e6EVOwsa+eLY78jo2CqYWzhGez9zn0D2LMKNmZQi88WuTFVw9r1GSKIHstoDWvn54zISmr/1JgjC++mv2yRvatcvs8GhcsZVZT8dueaNK6tXLd1fQumhXCjpMyFjOlPWVTBPjlnsC5Uh98V/YiIa898SF4dwfjtDrG/fQZYmWUzJ8k2AslYLKGs=
with this key:
12341234123412341234123412341234
with counter function described in the beginning of this post using crypto++. If you succeed post the decrypted text (which contains only numbers) and your solution please.
Update2:
I'm not providing an IV in python code, the python module ignores IV. I the IV thing is what causing the problem.
As I read their source codes I can say PyCrypto and Crypto++ Both are perfect libraries for cryptography for Python and C++. The problem was that I was prefixing the encrypted data with some meta information about file and I totally forgot about that, after handling these meta data in client Crypto++ decrypted my files.
As I didn't find this documented explicitly anywhere (not even in Wikipedia) I write it here:
Any combination of Nonce, IV and Counter like concatenation, xor, or likes will work for CTR mode, but the standard that most libraries implement is to concatenate these values in order. So the value that is used in block cipher algorithm is usually: Nonce + IV + Counter. And counter usually starts from 1 (not 0).

AES encryption in Node.js to match expected decryption in Python

I have the following Python script for decryption:
from Crypto.Cipher import AES
shared_secret = raw_input('Enter crypted_shared_secret: ').strip()
cipher = AES.new(shared_secret.decode('base64'), AES.MODE_ECB)
blob = raw_input('Enter crypted_blob: ').strip()
plain = cipher.decrypt(blob.decode('base64'))
print(plain)
I'm trying to generate the values that would produce the original blob using that script, using Node. Here is my attempt:
const Crypto = require('crypto');
var shared_secret = Crypto.randomBytes(32);
var cipher = Crypto.createCipher('aes-256-ecb', shared_secret);
crypted_blob = cipher.update(blob, 'utf8', 'base64') + cipher.final('base64');
I can only modify the Node.js script, but I'm not sure where it's going wrong.
You need to encode the shared secret key to Base64 only after you use it for encryption:
var shared_secret = Crypto.randomBytes(32);
var cipher = Crypto.createCipheriv('aes-256-ecb', shared_secret, "");
crypted_blob = cipher.update(blob, 'utf8', 'base64') + cipher.final('base64');
// send `shared_secret.toString('base64')`
Other problems:
crypto.createCipher assumes the shared secret is a password and not a key, which is why it will use a bad key derivation (OpenSSL-compatible).
Node.js' crypto module automatically applies PKCS#7 padding (same as PKCS#5 padding), but PyCrypto doesn't apply any padding on its own. So you either need to use the same unpadding in Python or you can disable padding in node.js with Cipher.setAutoPadding(false);, but then you will have to provide plaintexts that are a multiple of the block size (16 byte for AES).
Security considerations:
Never use ECB mode. It's deterministic and therefore not semantically secure. You should at the very least use a randomized mode like CBC or CTR. It is better to authenticate your ciphertexts so that attacks like a padding oracle attack are not possible. This can be done with authenticated modes like GCM or EAX, or with an encrypt-then-MAC scheme.

Pycrypto OpenPGP encryption: Why is there a 16byte limit to the key, what part of the key do I provide to hit 16 bytes?

I'm trying to encrypt a file using OpenPGP in python via the pycrypto application. I've been following the sample provided in their code here: https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Cipher/CAST.py
So I'm using mode.openPGP, but I can't seem to encrypt anything using a public key. My public key is well over the 16byte limit they specify (and any generation I've seen is over this limit as well). Is there a different value I'm supposed to use here, like the fingerprint ID?
I'm trying to read the contents of a file, encrypt it with a key, then print it into a new file to be sent (both will be deleted later on).
My code is as follows:
iv = CryptoRandom.new().read(CAST.block_size)
cipher = CAST.new(public_key, CAST.MODE_OPENPGP, iv)
file = open(filename)
contents = ''.join(file.readlines())
encrypted_contents = cipher.encrypt(contents)
encrypted_filename = filename.replace('/tmp/', '/tmp/encrypted')
encrypted_filename = encrypted_filename.replace('.csv', '.asc')
encrypted_file = open(encrypted_filename, 'w')
encrypted_file.write(encrypted_contents)
return encrypted_filename
I think you may be misunderstanding the algorithm you're using here. CAST is a symmetric-key algorithm, but whilst this implementation has an "OpenPGP mode", that doesn't mean that you simply pass your public key to it.
You should be generating a unique 16 byte key and passing that to CAST.new(). You would then generally encrypt that randomly-generated key using the public-key, and store/transmit the cipher text, and encrypted random-key together. The decryption process would decrypt the random-key using the private-key, then use the decrypted random-key to decrypt the cipher text.

Categories