📜 ⬆️ ⬇️

Python cryptography: encrypting information and creating digital signatures with the PyCrypto package



Long suffered from PyCrypto , this article as a result turned out and full implementation of the following protocol :

Sending step:
')
1. Alice signs the message with her digital signature and encrypts it with the Bob 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, a signature, and an encrypted session key.

Reception stage:

Bob receives Alice's encrypted message, a signature, and an encrypted session key.
4. Bob decrypts the session key with his private key.
5. Using the session key thus received, Bob decrypts Alice's encrypted message.
6. Bob decrypts and verifies Alice's signature.

The above protocol is a hybrid encryption system , which works 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). A symmetric algorithm is then used to encrypt the message. In the case of block encryption, it is necessary to use encryption mode (for example, CBC ), which will allow encrypting a message with a length exceeding the block length. As for the random key itself, it must be encrypted using the public key of the recipient of the message, and it is at this stage that the RSA public key is used .

Since the session key is short, its encryption takes a little time. Encrypting a set of messages using an asymmetric algorithm is a computationally more complex task, so symmetric encryption is preferable here. Then it is enough to send a message encrypted with a symmetric algorithm, as well as the corresponding key in an encrypted form. The recipient first decrypts the key using his secret key, and then receives the entire message with the received key.

Start by generating a pair of keys 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, an RSA-based encryption system is considered secure, starting with a key size of 2048 bits.

In the RSA system, you can create messages that will be both encrypted and digitally signed. 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 to it) using 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 function allows you to optimize the above digital signature algorithm. Not the message itself is encrypted, but the hash value taken from the message. This method provides the following benefits:

1. Reducing computational complexity. As a rule, the document is much larger than its hash.
2. Increased cryptographic strength. A cryptanalyst cannot, using the public key, pick up a signature for a message, but only for its hash.
3. Ensuring compatibility. Most algorithms operate on strings of data bits, but some use different representations. The hash function can be used to convert arbitrary input text into a suitable format.

Hash functions are functions, mathematical or other, that receive a variable length string (called a prototype) as input and convert it to a fixed, usually smaller, length string (called a hash value). The meaning of the hash function is to obtain the characteristic feature of the pre-image - the value by which various pre-images are analyzed when solving the inverse problem. A unidirectional hash function is a hash function that works only in one direction: it is easy to calculate the hash value by a prototype, but it is difficult to create a prototype whose hash value is equal to a given value. The hash function is open, the secret of its calculation does not exist. The security of a unidirectional hash function lies precisely in its unidirectionality.

We proceed to writing the first paragraph of the protocol:

1. Alice signs the message with her digital signature and encrypts it with the Bob 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() 


The following listing 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 AES symmetric algorithm).
3. The session key is encrypted with Bob’s public key (asymmetric RSA 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 number, as a rule it should be random or pseudo-random. Accident is crucial for achieving semantic security, which, when reusing a scheme with the same key, will not allow an attacker to infer relationships between segments of encrypted messages. The initialization vector is not encrypted and is saved before the encrypted message.

Next, Alice sends Bob an encrypted message, a signature, and an encrypted session key.

Bob receives Alice's encrypted message, a signature, and an encrypted session key.

4. Bob decrypts the session key with his private key.
5. Using the session key thus received, 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() 


Final stage:

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 problem of factoring the product of two large primes. 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 , which are relatively easy to calculate, but inverted with great difficulty. That is, knowing x is easy to calculate f (x), but from the known f (x) it is not easy to calculate x. Here, "not easy" means that it may take millions of years to calculate x over f (x), even if all the computers in the world will fight on this problem.

Literature:
Bruce Schneier - Applied Cryptography

Source: https://habr.com/ru/post/265309/


All Articles