Skip to main content

A Python encryption library implemented in Rust. It supports AEAD with AES-GCM and ChaCha20Poly1305. It uses ring crate to handle encryption.

Project description

REncrypt

A Python encryption library implemented in Rust. It supports AEAD with AES-GCM and ChaCha20Poly1305. It uses ring to handle encryption.
If offers slightly higher speed compared to other Python libs, especially for small chunks of data. The API also tries to be easy to use but it's more optimized for speed than usability.

So if you want to achieve the highest possible encryption speed, consider giving it a try.

Benchmark

Some benchmarks comparing to PyFLocker, which, from other implementations, I found to be the fastest.

Buffer in memory

This is useful when you keep a buffer, set your plaintext/ciphertext in there, and then encrypt/decrypt in-place that buffer. This is the most performant way to use it, because it does't copy any bytes nor allocate new memory.
REncrypt is faster on small buffers, less than few MB, PyFLocker is comming closer for larger buffers.

Encrypt seconds
Encrypt buffer

Decrypt seconds
Decrypt buffer

Block size and duration in seconds

MB REncrypt
encrypt
PyFLocker
encrypt
REncrypt
decrypt
PyFLocker
decrypt
0.03125 0.00001 0.00091 0.00001 0.00004
0.0625 0.00001 0.00005 0.00001 0.00004
0.125 0.00002 0.00005 0.00003 0.00005
0.25 0.00004 0.00008 0.00005 0.00009
0.5 0.00010 0.00014 0.00011 0.00015
1 0.00021 0.00024 0.00021 0.00029
2 0.00043 0.00052 0.00044 0.00058
4 0.00089 0.00098 0.00089 0.00117
8 0.00184 0.00190 0.00192 0.00323
16 0.00353 0.00393 0.00367 0.00617
32 0.00678 0.00748 0.00749 0.01348
64 0.01361 0.01461 0.01460 0.02697
128 0.02923 0.03027 0.03134 0.05410
256 0.06348 0.06188 0.06136 0.10417
512 0.11782 0.13463 0.12090 0.21114
1024 0.25001 0.24953 0.25377 0.42581

File

Encrypt seconds
Encrypt file

Decrypt seconds
Decrypt buffer

File size and duration in seconds

MB REncrypt
encrypt
PyFLocker
encrypt
REncrypt
decrypt
PyFLocker
decrypt
0.031251 0.00010 0.00280 0.00004 0.00479
0.062501 0.00009 0.00218 0.00005 0.00143
0.125 0.00020 0.00212 0.00014 0.00129
0.25 0.00034 0.00232 0.00020 0.00165
0.5 0.00050 0.00232 0.00035 0.00181
1 0.00087 0.00356 0.00065 0.00248
2 0.00215 0.00484 0.00154 0.00363
4 0.00361 0.00765 0.00301 0.00736
8 0.00688 0.01190 0.00621 0.00876
16 0.01503 0.02097 0.01202 0.01583
32 0.02924 0.03642 0.02563 0.02959
64 0.05737 0.06473 0.04431 0.05287
128 0.11098 0.12646 0.08944 0.09926
256 0.22964 0.24716 0.17402 0.19420
512 0.43506 0.46444 0.38143 0.38242
1024 0.97147 0.95803 0.78137 0.87363
2048 2.07143 2.10766 1.69471 2.99210
4096 4.85395 4.69722 5.40580 8.73779
8192 10.76984 11.76741 10.29253 21.00636
16384 21.84490 26.27385 39.56230 43.55530

Usage

There are three ways in which you can use the lib, the main difference is the speed, some offers an easier way to use it sacrificing performance.

  1. With a buffer in memory: using encrypt()/decrypt(), is useful when you keep a buffer (or have it from somewhere), set your plaintext/ciphertext in there, and then encrypt/decrypt in-place that buffer. This is the most performant way to use it, because it does't copy any bytes nor allocate new memory. If you can directly collect the data to that buffer, like buffered_reader.read_into(), this is the preffered way to go.
  2. From some bytes to the buffer: using encrypt_into()/decrypt_into(), when you have some arbitrary bytes that you want to work with. It will first copy those bytes to the buffer then do the operation in-place in buffer. This is a bit slower, especially for large data, because it needs to copy the bytes to the buffer.
  3. From some bytes to another new bytes: using encrypt_from()/decrypt_from(), it doesn't use the buffer at all, you just got some bytes you want to work with and you receive back another new bytes. This is the slowest one because it needs to first allocate a buffer, copy the data to the buffer, perform the operation then return that buffer as bytes. It's the easiest to use but is not so performant.

Examples

You can see more in examples directory and in bench.py which has some benchmarks. Here are few simple examples:

Encrypt and decrypt with a buffer in memory encrypt()/decrypt()

This is the most performant way to use it as it will not copy bytes to the buffer nor allocate new memory for plaintext and ciphertext.

from rencrypt import REncrypt, Cipher
import os

# You can use also other ciphers like `cipher = Cipher.ChaCha20Poly1305`.
cipher = Cipher.AES256GCM
key = cipher.generate_key()
enc = REncrypt(cipher, key)

# we get a buffer based on block len 4096 plaintext
# the actual buffer will be 28 bytes larger as in ciphertext we also include the tag and nonce
plaintext_len, ciphertext_len, buf = enc.create_buf(4096)
aad = b"AAD"

# put some plaintext in the buffer, it would be ideal if you can directly collect the data into the buffer without allocating new memory
# but for the sake of example we will allocate and copy the data
plaintext = os.urandom(plaintext_len)
# enc.copy_slice is slighlty faster than buf[:plaintext_len] = plaintext, especially for large plaintext, because it copies the data in parallel
enc.copy_slice(plaintext, buf)
 # encrypt it, this will encrypt in-place the data in the buffer
 print("encryping...")
ciphertext_len = enc.encrypt(buf, plaintext_len, 42, aad)
cipertext = buf[:ciphertext_len]
# you can do something with the ciphertext

# decrypt it
# if you need to copy ciphertext to buffer, we don't need to do it now as it's already in the buffer
# enc.copy_slice(ciphertext, buf[:len(ciphertext)])
print("decryping...")
plaintext_len = enc.decrypt(buf, ciphertext_len, 42, aad)
plaintext2 = buf[:plaintext_len]
assert plaintext == plaintext2
# best practice, you should always zeroize the plaintext and key after you are done with them
print("bye!")

You can use other ciphers like cipher = Cipher.ChaCha20Poly1305.

Encrypt and decrypt a file

import errno
import io
import os
from pathlib import Path
import shutil
from rencrypt import REncrypt, Cipher
import hashlib


def read_file_in_chunks(file_path, buf):
    with open(file_path, "rb") as file:
        buffered_reader = io.BufferedReader(file, buffer_size=len(buf))
        while True:
            read = buffered_reader.readinto(buf)
            if read == 0:
                break
            yield read


def calculate_file_hash(file_path):
    hash_algo = hashlib.sha256()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_algo.update(chunk)
    return hash_algo.hexdigest()


def compare_files_by_hash(file1, file2):
    return calculate_file_hash(file1) == calculate_file_hash(file2)


def silentremove(filename):
    try:
        os.remove(filename)
    except OSError as e:  # this would be "except OSError, e:" before Python 2.6
        if e.errno != errno.ENOENT:  # errno.ENOENT = no such file or directory
            raise  # re-raise exception if a different error occurred


def create_directory_in_home(dir_name):
    # Get the user's home directory
    home_dir = Path.home()

    # Create the full path for the new directory
    new_dir_path = home_dir / dir_name

    # Create the directory
    try:
        new_dir_path.mkdir(parents=True, exist_ok=True)
    except Exception as e:
        print(f"Error creating directory: {e}")

    return new_dir_path.absolute().__str__()


def create_file_with_size(file_path_str, size_in_bytes):
    with open(file_path_str, "wb") as f:
        for _ in range(size_in_bytes // 4096):
            f.write(os.urandom(4096))


def delete_dir(path):
    if os.path.exists(path):
        shutil.rmtree(path)
    else:
        print(f"Directory {path} does not exist.")


tmp_dir = create_directory_in_home("rencrypt_tmp")
fin = tmp_dir + "/" + "fin"
fout = tmp_dir + "/" + "fout.enc"
create_file_with_size(fin, 42 * 1024 * 1024)

chunk_len = 256 * 1024

key = os.urandom(32)

cipher = Cipher.AES256GCM
key = cipher.generate_key()
enc = REncrypt(cipher, key)
plaintext_len, _, buf = enc.create_buf(chunk_len)

aad = b"AAD"

# encrypt
print("encryping...")
with open(fout, "wb", buffering=plaintext_len) as file_out:
    i = 0
    for read in read_file_in_chunks(fin, buf[:plaintext_len]):
        ciphertext_len = enc.encrypt(buf, read, i, aad)
        file_out.write(buf[:ciphertext_len])
        i += 1
    file_out.flush()

# decrypt
print("decryping...")
tmp = fout + ".dec"
with open(tmp, "wb", buffering=plaintext_len) as file_out:
    i = 0
    for read in read_file_in_chunks(fout, buf):
        plaintext_len2 = enc.decrypt(buf, read, i, aad)
        file_out.write(buf[:plaintext_len2])
        i += 1
    file_out.flush()

compare_files_by_hash(fin, tmp)

delete_dir(tmp_dir)
# best practice, you should always zeroize the plaintext and key after you are done with them

print("bye!")

Encrypt and decrypt from some bytes to the buffer encrypt_into()/decrypt_into() or encrypt_into1()/decrypt_into1()

This is a bit slower than handling data only via the buffer, especially for large plaintext, but there are situations when you can't directly collect the data to the buffer but have some bytes from somewhere else.

For encrypt_into()/decrypt_into() the plaintext is bytes.

from rencrypt import REncrypt, Cipher
import os

# You can use also other ciphers like `cipher = Cipher.ChaCha20Poly1305`.
cipher = Cipher.AES256GCM
key = cipher.generate_key()
enc = REncrypt(cipher, key)

# we get a buffer based on block len 4096 plaintext
# the actual buffer will be 28 bytes larger as in ciphertext we also include the tag and nonce
plaintext_len, ciphertext_len, buf = enc.create_buf(4096)
aad = b"AAD"

plaintext = bytes(os.urandom(plaintext_len))

 # encrypt it, after this will have the ciphertext in the buffer
 print("encryping...")
ciphertext_len = enc.encrypt_into(plaintext, buf, 42, aad)
cipertext = bytes(buf[:ciphertext_len])

# decrypt it
print("decryping...")
plaintext_len = enc.decrypt_into(cipertext, buf, 42, aad)
plaintext2 = buf[:plaintext_len]
assert plaintext == plaintext2
# best practice, you should always zeroize the plaintext and key after you are done with them
print("bye!")

For encrypt_into1()/decrypt_into1() the only difference is that the input is bytearray.

Encrypt and decrypt from some bytes to another new bytes, without using the buffer encrypt_from()/decrypt_from() or encrypt_from1()/decrypt_from1()

This is the slowest option, especially for large plaintext, because it allocates new memory for the ciphertext on encrypt and plaintext on decrypt.

For encrypt_from()/decrypt_from() the plaintext is bytes.

from rencrypt import REncrypt, Cipher
import os

# You can use also other ciphers like `cipher = Cipher.ChaCha20Poly1305`.# You can use also other ciphers like `cipher = Cipher.ChaCha20Poly1305`.
cipher = Cipher.AES256GCM
key = cipher.generate_key()
enc = REncrypt(cipher, key)

aad = b"AAD"

plaintext = os.urandom(4096)

 # encrypt it, this will return the ciphertext
 print("encryping...")
ciphertext = enc.encrypt_from(plaintext, 42, aad)

# decrypt it
print("decryping...")
plaintext2 = enc.decrypt_from(ciphertext, 42, aad)
assert plaintext == plaintext2
# best practice, you should always zeroize the plaintext and key after you are done with them
print("bye!")

For encrypt_from1()/decrypt_from1() the only difference is that the input is bytearray.

Building from source

Browser

Open in Gitpod

Open in Codespaces

Geting sources from GitHub

git clone https://github.com/radumarias/rencrypt-python && cd rencrypt-python

Compile and run

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

To configure your current shell, you need to source the corresponding env file under $HOME/.cargo. This is usually done by running one of the following (note the leading DOT):

. "$HOME/.cargo/env"
python -m venv .env
source .env/bin/activate
pip install maturin
maturin develop
python bench.py

More benchmarks

Different ways to use the lib

Encrypt
Encrypt buffer

Decrypt
Decrypt buffer

Block size and duration in seconds

MB REncrypt
encrypt
PyFLocker
encrypt update_into
REncrypt
encrypt_into
REncrypt
encrypt_from
PyFLocker
encrypt update
REncrypt
decrypt
PyFLocker
decrypt update_into
REncrypt
decrypt_into
REncrypt
decrypt_from
PyFLocker
decrypt update
0.03125 0.00001 0.00091 0.00001 0.00002 0.00009 0.00001 0.00004 0.00001 0.00003 0.00005
0.0625 0.00001 0.00005 0.00002 0.00002 0.00005 0.00001 0.00004 0.00002 0.00004 0.00008
0.125 0.00002 0.00005 0.00003 0.00005 0.00011 0.00003 0.00005 0.00003 0.00007 0.00013
0.25 0.00004 0.00008 0.00007 0.00009 0.00019 0.00005 0.00009 0.00007 0.00016 0.00023
0.5 0.00010 0.00014 0.00015 0.00021 0.00035 0.00011 0.00015 0.00014 0.00033 0.00043
1 0.00021 0.00024 0.00080 0.00066 0.00082 0.00021 0.00029 0.00044 0.00081 0.00103
2 0.00043 0.00052 0.00082 0.00147 0.00147 0.00044 0.00058 0.00089 0.00162 0.00176
4 0.00089 0.00098 0.00174 0.00218 0.00284 0.00089 0.00117 0.00130 0.00273 0.00340
8 0.00184 0.00190 0.00263 0.00462 0.00523 0.00192 0.00323 0.00283 0.00484 0.00571
16 0.00353 0.00393 0.00476 0.01196 0.01410 0.00367 0.00617 0.00509 0.00834 0.01031
32 0.00678 0.00748 0.00904 0.02051 0.02440 0.00749 0.01348 0.01014 0.01780 0.02543
64 0.01361 0.01461 0.01595 0.03323 0.05064 0.01460 0.02697 0.01920 0.03355 0.05296
128 0.02923 0.03027 0.03343 0.06805 0.10362 0.03134 0.05410 0.03558 0.06955 0.11380
256 0.06348 0.06188 0.07303 0.13003 0.20911 0.06136 0.10417 0.07572 0.13733 0.20828
512 0.11782 0.13463 0.14283 0.26799 0.41929 0.12090 0.21114 0.14434 0.25771 0.41463
1024 0.25001 0.24953 0.28912 0.51228 0.82370 0.25377 0.42581 0.29795 0.53807 0.82588

Speed throughput

256KB seems to be the sweet spot for buffer size that offers the max MB/s speed for encryption, on benchmarks that seem to be the case. We performed 10.000 encryption operations for each size varying from 1KB to 16MB.

Speed throughput

MB MB/s
0.0009765625 1083
0.001953125 1580
0.00390625 2158
0.0078125 2873
0.015625 3348
0.03125 3731
0.0625 4053
0.125 4156
0.25 4247
0.5 4182
1.0 3490
2.0 3539
4.0 3684
8.0 3787
16.0 3924

For the future

  • Add more AES ciphers like AES128-GCM and AES-GCM-SIV
  • Add other encryption providers like RustCrypto and libsodium
  • Maybe add support for RSA and Elliptic-curve cryptography
  • Saving and loading keys from file

Considerations

This lib hasn't been audited, but it mostly wraps ring crate which is a well known library, so in principle it should offer the same level of security.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

REncrypt-0.1.8.tar.gz (2.2 MB view hashes)

Uploaded Source

Built Distribution

REncrypt-0.1.8-cp312-cp312-manylinux_2_34_x86_64.whl (332.3 kB view hashes)

Uploaded CPython 3.12 manylinux: glibc 2.34+ x86-64

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page