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
Decrypt seconds
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
Decrypt seconds
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.
- 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, likebuffered_reader.read_into()
, this is the preffered way to go. - From some bytes to the buffer: using
encrypt_into()
/decrypt_into()
, when you have some arbitrarybytes
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. - 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
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
Decrypt
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
.
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 likeAES128-GCM
andAES-GCM-SIV
- Add other encryption providers like RustCrypto and libsodium
- Maybe add support for
RSA
andElliptic-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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for REncrypt-0.1.8-cp312-cp312-manylinux_2_34_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 436f73763f9a6c719f5ac5817632aa3cb77674a1e1cf9f438c9bb0225ca1022b |
|
MD5 | 76b5e1a1676e91fb63a5141d4d63858f |
|
BLAKE2b-256 | 3190f11541031ecd837b59610995d69734437164b21f8b558aa239878d436fab |