Source code for crypt
"""On-the-fly AES256 CTR encryption with file-like interface."""
from Crypto.Cipher import AES
from Crypto.Util import Counter
import hashlib, os
[docs]def keygen(password: str, salt: str, iterations: int=10**6) -> bytes:
"""Generate a 32 byte key from password and salt using PBKDF2.
Args:
password (str): Password string (encoded to utf8)
salt (str): Salt (encoded to utf8)
iterations (int): Number of iterations, 1M is the default
Returns:
bytes: 32 byte key
"""
return hashlib.pbkdf2_hmac('sha256', password.encode('utf8'),
salt.encode('utf8'), iterations)
[docs]class AESFile:
"""On-the-fly AES encryption (on read) and decryption (on write).
Uses CTR mode with 16 byte initial value (iv). When reading,
returns the iv first, then encrypted payload. On writing, first
16 bytes are assumed to contain the iv.
Does the bare minimum, you may get errors if not careful. See
Python's :class:`io.IOBase` for details on most methods.
Args:
filename (str): File to open for reading (encrypt on the fly)
or writing (decrypt on the fly)
mode (str): Either 'rb' or 'wb', just like with :func:`io.open`
key (bytes): Encryption/decryption key (32 bytes for AES256)
iv (bytes): Initial value (16 bytes), if not set uses os.urandom
Returns:
AESFile: File-like object
"""
def __initAES(self) -> None:
self.obj = AES.new(self.key, AES.MODE_CTR, counter=Counter.new(
128, initial_value=int.from_bytes(self.iv, byteorder='big')))
def __init__(self, filename: str, mode: str, key: bytes, iv: bytes=None) -> None:
"""Init the class. Documented in class docstring."""
if not mode in ('wb', 'rb'):
raise RuntimeError('Only rb and wb modes supported!')
self.pos = 0
self.key = key
self.mode = mode
self.fp = open(filename, mode)
if mode == 'rb':
self.iv = iv or os.urandom(16)
self.__initAES()
else: self.iv = bytearray(16)
def __enter__(self) -> None:
return self
def __exit__(self, type, value, traceback) -> None:
self.fp.close()
[docs] def write(self, data : bytes) -> int:
"""Write data and decrypt on the fly. First 16 bytes absorbed as iv."""
datalen = len(data)
if self.pos < 16:
ivlen = min(16-self.pos, datalen)
self.iv[self.pos:self.pos+ivlen] = data[:ivlen]
self.pos += ivlen
if self.pos == 16: self.__initAES() # ready to init now
data = data[ivlen:]
if data: self.pos += self.fp.write(self.obj.decrypt(data))
return datalen
[docs] def read(self, size: int=-1) -> bytes:
"""Read data and encrypt on the fly. First 16 bytes returned are iv."""
ivpart = b''
if self.pos < 16:
if size == -1: ivpart = self.iv
else:
ivpart = self.iv[self.pos:min(16, self.pos+size)]
size -= len(ivpart)
enpart = self.obj.encrypt(self.fp.read(size)) if size else b''
self.pos += len(ivpart) + len(enpart)
return ivpart + enpart
[docs] def tell(self) -> int:
"""Tell the current position.
Note that when reading, goes 16 bytes further than the file
being read, due to the fact that iv is injected to start.
"""
return self.pos
# only in read mode (encrypting)
[docs] def seek(self, offset: int, whence: int=0) -> None:
"""Seek to given position.
Only offset 0 is supported (relative to start, current position
or end depending on whence parameter). Otherwise dummy-encrypting
stuff might get really slow.
Args:
offset (int): Offset, has to be 0
whence (int): 0,1,2 for absolute,relative,end-based
Raises:
RuntimeError: If offset is nonzero
"""
if offset: raise RuntimeError('Only seek(0, whence) supported')
self.fp.seek(offset, whence) # offset=0 works for all whences
if whence==0: # absolute positioning, offset=0
self.pos = 0
self.__initAES()
elif whence==2: # relative to file end, offset=0
self.pos = 16 + self.fp.tell()
[docs] def close(self) -> None:
"""Close the file stream."""
self.fp.close()