[PYTHON] Reasons why hybrid encryption is used (comparison of encryption / decryption speeds)

It is known that encryption / decryption by public key method (RSA) takes time, and hybrid encryption is used. I actually measured the speed of encryption and decryption, also as a memo of how to encrypt and decrypt with a program (Python).

1. Types of encryption

1.1. Symmetric-key cryptography (common key) method

A method that uses the same key for encryption and decryption.

AES (block cipher)

The block length is fixed at 128 bits, and the key length can be 128 bits, 192 bits, or 256 bits.

Cipher mode of operation

A method for encrypting information longer than the block length with a block cipher.

1.2. Asymmetric cryptography (public key) method

A method of encrypting with a public key and decrypting with a private key.

RSA crypto

Cryptography that utilizes the difficulty of discrete logarithms and prime factorization of large numbers. Ciphertext = (plaintext ** E)% N The {E, N} pair of is the public key at the time of encryption. Plaintext = (ciphertext ** D)% N The {D, N} pair of is the private key at the time of decryption. Use a number of 2048 bits or more as N. 4096bit or more (ref. NIST SP800-57) for new use after 2031. Even with public key cryptography, communication can be interrupted if a malicious person enters between the sender and receiver of the communication (MITM (man-in-the-middle) attack). A public key certificate is used to prevent this. RSA-OAEP uses random numbers to generate different ciphertexts for the same plaintext each time.

Elliptic curve cryptography

Even shorter keys are stronger than RSA (the elliptic curve cryptography of 224 to 225 bit keys is equivalent to the same strength as 2048 bit RSA).

2. Hybrid encryption (combination of public key method and common key method)

When encrypting a large file of several MB or more, the common key method is used because it takes a long time to encrypt and decrypt the public key method. The public key method is used to communicate the common key used at this time. It is said that the same level of encryption strength is preferable for both, and NIST Special Publication 800-57 Part 1 Table 2 of Revision 4, "Recommendations for Key Management" lists the security strengths that can be used as a reference.

Security strength Private key (common key) algorithm IFC (eg RSA)
128 AES-128 k = 3072
192 AES-192 k = 7680
256 AES-256 k = 15360

3. Comparison of actual processing time

The processing times of AES-256 and RSA-2048 were compared. (Although the strength is different ...)

Operating environment

3.1 Encryption and decryption with symmetric encryption (AES-256)

Refer to this site and look like AES256.py below. (It is necessary to delete the padding when actually using it for file encryption / decryption).

When you run AES256.py shown below,


$ python3 AES256.py 
File size =  10.0 [MB]
Encode:
AES_encrypt_time:0.12138915061950684[sec]
Decode:
AES_decrypt_time:0.12209415435791016[sec]

The result is as follows. You can see that it takes about 0.12 seconds to encrypt and decrypt a 10MB file.

AES256.py


import sys,struct,random
from Crypto.Cipher import AES
from hashlib import sha256
import time
import os

def generate_salt(digit_num):
    DIGITS_AND_ALPHABETS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    return "".join(random.sample(DIGITS_AND_ALPHABETS, digit_num))

# AES-256
def derive_key_and_iv(password, salt, bs):
    salted = ''.encode()
    dx = ''.encode()
    #AES from password-Key for 256 and initialization vector for CBC(iv)Generate a
    while len(salted) < 48: # 48 =AES256 key length(32byte)+IV length(16byte)
        hash = dx + password.encode() + salt.encode()
        dx = sha256(hash).digest()
        salted = salted + dx
    key = salted[0:32] # 32byte -> AES-256 key lengths
    iv = salted[32:48] # 16byte (AES.block_Same size as size,128bit in AES(=16byte)Fixed)
    return key, iv

#encryption
def encrypt(in_file, out_file, password):
    bs = AES.block_size
    #salt = generate_salt() #Random.new().read(bs - len('Salted__'))
    salt = generate_salt(AES.block_size)
    key, iv = derive_key_and_iv(password, salt, bs)

    cipher = AES.new(key, AES.MODE_CBC, iv) #Set CBC mode. Get the AESCipher class.
    out_file.write(('Salted__' + salt).encode()) #salt writes to encrypted file
    finished = False
    while not finished:
        chunk = in_file.read(1024 * bs)
        orgChunkLen = len(chunk)
        if len(chunk) == 0 or len(chunk) % bs != 0:
            padding_length = (bs - len(chunk) % bs) or bs
            padding = padding_length * chr(padding_length)
            chunk += padding.encode()
            finished = True
        if len(chunk) > 0:
            out_file.write(cipher.encrypt(chunk))

#Decryption
def decrypt(in_file, out_file, password):
    bs = AES.block_size
    in_file.seek(len('Salted__'))
    salt = in_file.read(16).decode()
    #key from salt and password,Get iv.
    key, iv = derive_key_and_iv(password, salt, bs)

    cipher = AES.new(key, AES.MODE_CBC, iv) #Set CBC mode. Get the AESCipher class.
    finished = False

    while not finished:
        chunk = in_file.read(1024 * bs)
        orgChunkLen = len(chunk)
        if orgChunkLen == 0 or orgChunkLen % bs != 0:
            padding_length = (bs - orgChunkLen % bs) or bs
            padding = padding_length * chr(padding_length)
            chunk += padding.encode()
            finished = True
        if orgChunkLen > 0:
            out_file.write(cipher.decrypt(chunk)[0:orgChunkLen])

def main(filename):
    infile = open(filename, "rb")
    outfile = open(filename+"_AES.bin", "wb")

    print("File size = ", os.path.getsize(filename) /1024/1024, "[MB]")
    print("Encode:")
    start = time.time()
    encrypt(infile,outfile,"password")
    # openssl enc -e -aes-256-cbc -salt -k "password" -in practice.bin -out practice_aes.bin
    elapsed_time = time.time() - start
    print ("AES_encrypt_time:{0}".format(elapsed_time) + "[sec]")

    infile.close()
    outfile.close()

    print("Decode:")

    outinfile = open(filename+"_AES.bin", "rb")
    outfile2 = open(filename+"_dec_AES.bin", "wb")

    start = time.time()
    decrypt(outinfile,outfile2,"password")
    elapsed_time = time.time() - start
    print ("AES_decrypt_time:{0}".format(elapsed_time) + "[sec]")

    outinfile.close()
    outfile2.close()

if __name__== "__main__":
  filename = "practice.bin"
  main(filename)

3.2. Encryption and decryption using asymmetric cryptography (public key) method (RSA)

Basically RSA is not intended to be used to encrypt large amounts of data. It was implemented in chunks for speed measurement, but it is not generally recommended. Large size data is encrypted using symmetric encryption such as AES.

Code modified with reference to this site and chunked 196 bytes. Changed to encrypt in units.

When you run RSA2048.py shown below,


$ python3 RSA2048.py 
max_data_len= 196
Generate Key:
privatePem len =  1674
publicPem len =  450
Load Key:
File size =  10.0 [MB]
Encode:
RSA_encrypt_time:46.78378772735596[sec]
Decode:
RSA_decrypt_time:123.22586274147034[sec]

The result is as follows. You can see that it takes about 47 seconds to encrypt a 10MB file and about 123 seconds to decrypt it. Since each AES took about 0.12 seconds, it can be seen that RSA is about 400 times slower for encryption and about 1000 times slower for decryption.

RSA2048.py



from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import base64
import time
import os

modulus_length = 2048 # bit
max_data_len = int((int(modulus_length/8.0) - 11 )*0.8)  #The maximum size that can be encrypted with RSA is smaller than the key size. Depends on what you use for padding.
print("max_data_len=",max_data_len)

def generate_keys():
    key = RSA.generate(modulus_length)
    pub_key = key.publickey()
    return key, pub_key

def encrypt_private_key(a_message, private_key):
    encryptor = PKCS1_OAEP.new(private_key)
    encrypted_msg = encryptor.encrypt(a_message)
    encoded_encrypted_msg = base64.b64encode(encrypted_msg)
    return encoded_encrypted_msg

def decrypt_public_key(encoded_encrypted_msg, public_key):
    encryptor = PKCS1_OAEP.new(public_key)
    decoded_encrypted_msg = base64.b64decode(encoded_encrypted_msg)
    decoded_decrypted_msg = encryptor.decrypt(decoded_encrypted_msg)
    return decoded_decrypted_msg

def encrypt_file(in_file, out_file, key):
    finished =False
    while not finished:
        chunk = in_file.read(max_data_len)
        if len(chunk) == 0 or len(chunk)%max_data_len:
            finished = True
        encdata = encrypt_private_key(chunk, key)
        a_number = len(encdata)
        out_file.write(a_number.to_bytes(4, byteorder='little'))
        out_file.write(encdata)
    out_file.close()

def decrypt_file(in_file, out_file, key):
    finished =False
    while not finished:
        bnum = in_file.read(4)
        inum = int.from_bytes(bnum, byteorder='little')
        chunk = in_file.read(inum)
        if len(chunk) == 0 or len(chunk)%inum:
            finished = True
        if len(chunk) != 0:
          decdata = decrypt_public_key(chunk, key)
          out_file.write(decdata[0:len(chunk)])
    out_file.close()

def main(filename):
    print("Generate Key:")
    private, public = generate_keys()
    privateFile = open("private.pem","wb")
    privatePem = private.exportKey(format='PEM')
    print("privatePem len = ", len(privatePem))
    privateFile.write(privatePem)
    privateFile.close()
    publicFile = open("public.pem","wb")
    publicPem = public.exportKey(format='PEM')
    print("publicPem len = ", len(publicPem))
    publicFile.write(publicPem)
    publicFile.close()
    #print (private)
    #message = b'AES password or key'
    #print(message)
    #encoded = encrypt_private_key(message, public)
    #decrypt_public_key(encoded, private)

    print("Load Key:")
    privateFile = open("private.pem","rb")
    private_pem = privateFile.read()
    privateFile.close()
    private_key = RSA.importKey(private_pem)
    publicFile = open("public.pem","rb")
    public_pem = publicFile.read()
    publicFile.close()
    public_key = RSA.importKey(public_pem)

    print("File size = ", os.path.getsize(filename) /1024/1024, "[MB]")

    print("Encode:")
    infile = open(filename, "rb")
    outfile = open(filename+"_RSA.bin", "wb")
    start = time.time()
    encrypt_file(infile,outfile,public_key)
    elapsed_time = time.time() - start
    print ("RSA_encrypt_time:{0}".format(elapsed_time) + "[sec]")
    infile.close()
    outfile.close()

    print("Decode:")
    infile = open(filename+"_RSA.bin", "rb")
    outfile = open(filename+"_dec_RSA.bin", "wb")
    start = time.time()
    decrypt_file(infile,outfile,private_key)
    elapsed_time = time.time() - start
    print ("RSA_decrypt_time:{0}".format(elapsed_time) + "[sec]")
    infile.close()
    outfile.close()


if __name__== "__main__":
    filename = "practice.bin"
    main(filename)

4. Summary

In actual TSL and PGP (GnuPG), the key of symmetric cryptography (equivalent data) is exchanged by public key method, and symmetric cryptography is used for the actual data, but this has a big difference in processing speed and is open to the public. It is said that encryption / decryption of the key method takes time. Here, in order to confirm how much the processing speed actually differs at the order level, we created and compared Python test code for symmetric cryptography (AES) and public key cryptography (RSA). As a result of actual measurement, it took about 0.12 seconds for AES to encrypt / decrypt a 10MB file, while RSA took about 47 seconds to encrypt and about 400 times slower, and decryption was about 123. It turned out to be about 1000 times slower in seconds.

Reference books

Recommended Posts

Reasons why hybrid encryption is used (comparison of encryption / decryption speeds)
FAQ: Why is the comparison of numbers inconsistent?
Why is cross entropy used for the objective function of the classification problem?
Comparison of matrix transpose speeds with Python