
Python Cryptography: Encrypting Information and Creating Digital Signatures Using PyCrypto
- From the sandbox
- Tutorial

I was tormented for a long time with PyCrypto , as a result, this article turned out and the full implementation of the following protocol :
Sending stage:
1. Alice signs the message with her digital signature and encrypts it with Bob's public key ( asymmetric algorithm ).
2. Alice generates a random session key and encrypts the message with this key (using a symmetric algorithm ).
3. The session key is encrypted with Bob's public key (asymmetric algorithm).
Alice sends Bob an encrypted message, signature, and encrypted session key.
Stage of admission:
Bob receives Alice's encrypted message, signature, and encrypted session key.
4. Bob decrypts the session key with his private key.
5. Using the thus obtained session key, Bob decrypts Alice's encrypted message.
6. Bob decrypts and verifies Alice's signature.
The above protocol is a hybrid encryption system that operates as follows. For a symmetric AES algorithm (or any other), a random session key is generated.
Such a key usually has a size from 128 to 512 bits (depending on the algorithm). Then a symmetric algorithm is used to encrypt the message. In the case of block encryption, you must useencryption mode (for example, CBC ), which allows you to encrypt a message with a length exceeding the length of the block. As for the random key itself, it must be encrypted using the public key of the message recipient, and it is at this stage that the RSA public key cryptosystem is used .
Because the session key is short, it takes a little time to encrypt it. Encrypting a set of messages using an asymmetric algorithm is a computationally more complex task, so it is preferable to use symmetric encryption. Then it is enough to send a message encrypted by a symmetric algorithm, as well as the corresponding key in encrypted form. The recipient first decrypts the key using his private key, and then with the help of the received key he receives the entire message.
Let's start by generating a key pair for Alice and Bob.
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
# key generation Alisa
privatekey = RSA.generate(2048)
f = open('c:\cipher\\alisaprivatekey.txt','wb')
f.write(bytes(privatekey.exportKey('PEM'))); f.close()
publickey = privatekey.publickey()
f = open('c:\cipher\\alisapublickey.txt','wb')
f.write(bytes(publickey.exportKey('PEM'))); f.close()
# key generation Bob
privatekey = RSA.generate(2048)
f = open('c:\cipher\\bobprivatekey.txt','wb')
f.write(bytes(privatekey.exportKey('PEM'))); f.close()
publickey = privatekey.publickey()
f = open('c:\cipher\\bobpublickey.txt','wb')
f.write(bytes(publickey.exportKey('PEM'))); f.close()
At the moment, the RSA-based encryption system is considered reliable, starting with a key size of 2048 bits.
In the RSA system, you can create messages that will be encrypted and contain a digital signature. To do this, the author must first add his digital signature to the message, and then encrypt the resulting pair (consisting of the message itself and the signature) with the public key belonging to the recipient. The recipient decrypts the received message with his private key and verifies the author’s signature with his public key.
Using a one-way cryptographic hash functionallows you to optimize the above digital signature algorithm. It encrypts not the message itself, but the values of the hash function taken from the message. This method provides the following advantages:
1. Lower computational complexity. As a rule, a document is much larger than its hash.
2. Increase cryptographic strength. Using a public key, a cryptanalyst cannot pick up a signature for a message, but only for its hash.
3. Ensuring compatibility. Most algorithms operate on data bit strings, but some use different representations. The hash function can be used to convert arbitrary input text to a suitable format.
Hash functions are functions, mathematical or otherwise, that receive an input string of variable length (called the preimage) and convert it to a string of fixed, usually shorter, length (called the value of the hash function). The meaning of the hash function is to obtain a characteristic attribute of the inverse image - the value by which various inverse images are analyzed when solving the inverse problem. A unidirectional hash function is a hash function that works in only one direction: it is easy to calculate the value of the hash function from the inverse image, but it is difficult to create an inverse image whose hash value is equal to a given value. The hash function is open, the secrets of its calculation do not exist. The security of a one-way hash function lies precisely in its one-way function.
We proceed to write the first paragraph of the protocol:
1. Alice signs the message with her digital signature and encrypts it with Bob's public key (RSA asymmetric algorithm).
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
# creation of signature
f = open('c:\cipher\plaintext.txt','rb')
plaintext = f.read(); f.close()
privatekey = RSA.importKey(open('c:\cipher\\alisaprivatekey.txt','rb').read())
myhash = SHA.new(plaintext)
signature = PKCS1_v1_5.new(privatekey)
signature = signature.sign(myhash)
# signature encrypt
publickey = RSA.importKey(open('c:\cipher\\bobpublickey.txt','rb').read())
cipherrsa = PKCS1_OAEP.new(publickey)
sig = cipherrsa.encrypt(signature[:128])
sig = sig + cipherrsa.encrypt(signature[128:])
f = open('c:\cipher\signature.txt','wb')
f.write(bytes(sig)); f.close()
In the following listing, the code for the following two points of the protocol:
2. Alice generates a random session key and encrypts the message with this key (using the symmetric AES algorithm).
3. The session key is encrypted with Bob's public key (RSA asymmetric algorithm).
from Crypto.Cipher import AES
from Crypto import Random
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
# creation 256 bit session key
sessionkey = Random.new().read(32) # 256 bit
# encryption AES of the message
f = open('c:\cipher\plaintext.txt','rb')
plaintext = f.read(); f.close()
iv = Random.new().read(16) # 128 bit
obj = AES.new(sessionkey, AES.MODE_CFB, iv)
ciphertext = iv + obj.encrypt(plaintext)
f = open('c:\cipher\plaintext.txt','wb')
f.write(bytes(ciphertext)); f.close()
# encryption RSA of the session key
publickey = RSA.importKey(open('c:\cipher\\bobpublickey.txt','rb').read())
cipherrsa = PKCS1_OAEP.new(publickey)
sessionkey = cipherrsa.encrypt(sessionkey)
f = open('c:\cipher\sessionkey.txt','wb')
f.write(bytes(sessionkey)); f.close()
The CFB encryption mode requires an initialization vector (IV) (variable iv).
In cryptography, the initialization vector (IV) is a certain number, as a rule it should be random or pseudo-random. Randomness is critical to achieving semantic security, which when reusing a scheme under the same key will not allow an attacker to infer relationships between segments of encrypted messages. The initialization vector is not encrypted and is stored before the encrypted message.
Next, Alice sends Bob an encrypted message, signature, and encrypted session key.
Bob receives Alice's encrypted message, signature, and encrypted session key.
4. Bob decrypts the session key with his private key.
5. Using the thus obtained session key, Bob decrypts Alice's encrypted message.
from Crypto.Cipher import AES
from Crypto import Random
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
# decryption session key
privatekey = RSA.importKey(open('c:\cipher\\bobprivatekey.txt','rb').read())
cipherrsa = PKCS1_OAEP.new(privatekey)
f = open('c:\cipher\sessionkey.txt','rb')
sessionkey = f.read(); f.close()
sessionkey = cipherrsa.decrypt(sessionkey)
# decryption message
f = open('c:\cipher\plaintext.txt','rb')
ciphertext = f.read(); f.close()
iv = ciphertext[:16]
obj = AES.new(sessionkey, AES.MODE_CFB, iv)
plaintext = obj.decrypt(ciphertext)
plaintext = plaintext[16:]
f = open('c:\cipher\plaintext.txt','wb')
f.write(bytes(plaintext)); f.close()
Last step:
6. Bob decrypts and verifies Alice's signature.
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
# decryption signature
f = open('c:\cipher\signature.txt','rb')
signature = f.read(); f.close()
privatekey = RSA.importKey(open('c:\cipher\\bobprivatekey.txt','rb').read())
cipherrsa = PKCS1_OAEP.new(privatekey)
sig = cipherrsa.decrypt(signature[:256])
sig = sig + cipherrsa.decrypt(signature[256:])
# signature verification
f = open('c:\cipher\plaintext.txt','rb')
plaintext = f.read(); f.close()
publickey = RSA.importKey(open('c:\cipher\\alisapublickey.txt','rb').read())
myhash = SHA.new(plaintext)
signature = PKCS1_v1_5.new(publickey)
test = signature.verify(myhash, sig)
RSA security is based on the complexity of the factorization problem of the product of two large prime numbers. Factoring integers for large numbers is a task of great complexity. There is no known way to solve this problem quickly. Factorization is a candidate for one-way functions that are relatively easy to compute but invert with great difficulty. That is, knowing x is easy to calculate f (x), but the known f (x) is not easy to calculate x. Here, “not easy” means that it may take millions of years to calculate x by f (x), even if all the computers in the world are struggling with this problem.
References:
Bruce Schneier - Applied Cryptography