"""This module stores all keys and key making functions."""
from os import urandom
from random import SystemRandom
from hmac import compare_digest, HMAC
from typing import (
AsyncGenerator,
Union, Optional
)
from base64 import (
urlsafe_b64encode,
urlsafe_b64decode
)
from .errors import IncorrectKey
from . import defaults
from .crypto import (
AESwState as AES, FAST_ENCRYPTION,
Salt, BoxSalt, FileSalt)
try:
from hashlib import sha256, scrypt
except ImportError: # No Scrypt installed
if FAST_ENCRYPTION:
from cryptography.hazmat.primitives.kdf.scrypt\
import Scrypt as cryptography_Scrypt
def scrypt(
password: bytes, *, salt=None, n=None,
r=None, p=None, maxmem=0, dklen=64): # pylint: disable=unused-argument
"""
This is a little wrapper around the Scrypt
from the cryptography library.
"""
s = cryptography_Scrypt(salt=salt, length=dklen, n=n, r=r, p=p)
return s.derive(password)
else:
if not defaults.READTHEDOCS: # ReadTheDocs does not have Scrypt in hashlib
raise RuntimeError('Could not find Scrypt. Install tgbox[fast]')
if FAST_ENCRYPTION: # Is faster and more secure
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import PublicFormat
from cryptography.hazmat.primitives.serialization import Encoding
else:
from ecdsa.ecdh import ECDH
from ecdsa.curves import SECP256k1
from ecdsa.keys import SigningKey, VerifyingKey
__all__ = [
'Phrase',
'Key',
'BaseKey',
'MainKey',
'RequestKey',
'ShareKey',
'ImportKey',
'FileKey',
'EncryptedMainkey',
'DirectoryKey',
'HMACKey',
'make_basekey',
'make_mainkey',
'make_filekey',
'make_requestkey',
'make_sharekey',
'make_importkey',
'make_dirkey',
'make_hmackey'
]
[docs]
class Phrase:
"""This class represents passphrase"""
def __init__(self, phrase: Union[bytes, str]):
if isinstance(phrase, str):
self._phrase = phrase.encode()
elif isinstance(phrase, bytes):
self._phrase = phrase
else:
raise TypeError('phrase must be Union[bytes, str]')
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f'{class_name}({repr(self._phrase)}) # at {hex(id(self))}'
def __str__(self) -> str:
return self._phrase.decode()
def __hash__(self) -> int:
return hash((self._phrase, self.__class__.__name__))
def __eq__(self, other) -> bool:
return hash(self) == hash(other)
@property
def phrase(self) -> bytes:
"""Returns current raw phrase"""
return self._phrase
[docs]
@classmethod
def generate(cls, words_count: int=6) -> 'Phrase':
"""
Generates passphrase
Arguments:
words_count (``int``, optional):
Words count in ``Phrase``.
"""
sysrnd = SystemRandom(urandom(32))
with open(defaults.WORDS_PATH,'rb') as words_file:
words_list = words_file.readlines()
phrase = [
sysrnd.choice(words_list).strip()
for _ in range(words_count)
]
return cls(b' '.join(phrase))
[docs]
class Key:
"""Metaclass that represents all keys."""
def __init__(self, key: bytes, key_type: int):
"""
Arguments:
key (``bytes``):
Raw bytes key.
key_type (``int``):
Type of key, where:
1: ``BaseKey``
2: ``MainKey``
3: ``RequestKey``
4: ``ShareKey``
5: ``ImportKey``
6: ``FileKey``
7: ``EncryptedMainkey``
8: ``DirectoryKey``
9: ``HMACKey``
"""
self._key = key
self._key_type = key_type
self._key_types = {
1: 'BaseKey',
2: 'MainKey',
3: 'RequestKey',
4: 'ShareKey',
5: 'ImportKey',
6: 'FileKey',
7: 'EncryptedMainkey',
8: 'DirectoryKey',
9: 'HMACKey'
}
def __hash__(self) -> int:
return hash((self._key, self._key_type))
def __eq__(self, other) -> bool:
return (
isinstance(other, self.__class__)\
and self._key_type == other.key_type\
and compare_digest(self._key, other.key)
)
def __repr__(self) -> str:
return f'{self._key_types[self._key_type]}({self._key}) # at {hex(id(self))}'
def __add__(self, other) -> bytes:
if isinstance(other, Salt):
return self._key + other.salt
return self._key + other
def __len__(self) -> int:
return len(self._key)
def __getitem__(self, key) -> int:
return self._key[key]
def __iter__(self) -> AsyncGenerator[int, None]:
for i in self._key:
yield i
@property
def key_types(self) -> dict:
"""Returns all key types"""
return self._key_types.copy()
@property
def key_type(self) -> int:
"""Returns current key type"""
return self._key_type
@property
def key(self) -> bytes:
"""Returns key in raw"""
return self._key
[docs]
@classmethod
def decode(cls, encoded_key: str) -> Union[
'BaseKey','MainKey','RequestKey',
'ShareKey','ImportKey','FileKey',
'EncryptedMainkey', 'DirectoryKey',
'HMACKey']:
"""
Decodes Key by prefix and returns
``Key`` in one of ``Key`` classes.
B: ``BaseKey``
M: ``MainKey``
R: ``RequestKey``
S: ``ShareKey``
I: ``ImportKey``
F: ``FileKey``
E: ``EncryptedMainkey``
D: ``DirectoryKey``
H: ``HMACKey``
Key example:
``MSGVsbG8hIEkgYW0gTm9uISBJdCdzIDI5LzExLzIwMjE=``.
You can decode it with ``Key.decode``.
"""
try:
ekey_types = {
'B': BaseKey, 'M': MainKey,
'R': RequestKey, 'S': ShareKey,
'I': ImportKey, 'F': FileKey,
'E': EncryptedMainkey,
'D': DirectoryKey,
'H': HMACKey
}
ekey_type = ekey_types[encoded_key[0]]
return ekey_type(urlsafe_b64decode(encoded_key[1:]))
except Exception as e:
raise IncorrectKey() from e
[docs]
def encode(self) -> str:
"""Encode raw key with ``urlsafe_b64encode`` and add prefix."""
prefix = self._key_types[self._key_type][0]
return prefix + urlsafe_b64encode(self._key).decode()
[docs]
def hex(self) -> str:
"""Returns key in hex representation"""
return self._key.hex()
[docs]
class BaseKey(Key):
"""
This ``Key`` used for ``MainKey`` creation and
cloned ``RemoteBox`` decryption. In API
it's usually result of ``keys.make_basekey``.
"""
def __init__(self, key: bytes):
super().__init__(key, 1)
[docs]
class MainKey(Key):
"""
``MainKey`` may be referred as "Box key". This
key encrypts all box data and used in ``FileKey``
creation. It's one of your most important ``Key``,
as leakage of it will result in compromising all
your encrypted files in *RemoteBox* & *LocalBox*.
When you clone other's *RemoteBox*, Session data
will be encrypted by ``BaseKey``, not ``MainKey``.
Usually you will see this ``Key`` as a result of
``keys.make_mainkey`` function.
"""
def __init__(self, key: bytes):
super().__init__(key, 2)
[docs]
class RequestKey(Key):
"""
The ``RequestKey`` is a key that *Requester*
creates when [s]he wants to import *Giver's*
file, directory or even clone other's
*RemoteBox* and access all files.
With ``RequestKey`` *Giver* makes ``ShareKey``.
Run ``help(tgbox.keys.make_requestkey)`` for information.
"""
def __init__(self, key: bytes):
super().__init__(key, 3)
[docs]
class ShareKey(Key):
"""
The ``ShareKey`` is a key that *Giver* creates
when [s]he wants to share file, directory or
even the whole *Box* with the *Requester*.
With ``ShareKey`` *Requester* makes ``ImportKey``.
Run ``help(tgbox.keys.make_sharekey)`` for information.
"""
def __init__(self, key: bytes):
super().__init__(key, 4)
[docs]
class ImportKey(Key):
"""
The ``ImportKey`` is a key that *Requester*
obtains after calling ``make_importkey``
function with the ``ShareKey``. This is
a decryption key for the requested object.
Run ``help(tgbox.keys.make_importkey)`` for information.
"""
def __init__(self, key: bytes):
super().__init__(key, 5)
[docs]
class FileKey(Key):
"""
``FileKey`` is a key that used for encrypting
file's bytedata and its metadata on upload. The
``FileKey`` encrypts all of *secret metadata* values
except the ``efile_path`` (encrypted file path), so
user with which you share file from your *Box*
will not know from which directory it was extracted.
.. note::
Usually you will not work with this class, API
converts ``DirectoryKey`` to ``FileKey`` under the hood,
but you can make it with ``tgbox.keys.make_filekey``.
"""
def __init__(self, key: bytes):
super().__init__(key, 6)
[docs]
class EncryptedMainkey(Key):
"""
This class represents encrypted mainkey. When
you clone other's *RemoteBox* we encrypt its
``MainKey`` with your ``BaseKey``.
"""
def __init__(self, key: bytes):
super().__init__(key, 7)
[docs]
class DirectoryKey(Key):
"""
``DirectoryKey`` is a ``Key`` that was added in the
``v1.3``. In previous versions, ``FileKey`` was
generated with the *SHA256* over the ``MainKey``
and ``FileSalt``. Now we will make it with the
``DirectoryKey``. See Docs for more information.
"""
def __init__(self, key: bytes):
super().__init__(key, 8)
[docs]
class HMACKey(Key):
"""
``HMACKey`` is a ``Key`` that is used to make a
*HMAC* of the bytestring. Typically, ``HMACKey``
is a result of a ``tgbox.keys.make_hmackey`` func.
"""
def __init__(self, key: bytes):
super().__init__(key, 9)
[docs]
def make_basekey(
phrase: Union[bytes, Phrase],
*,
salt: Union[bytes, int] = defaults.Scrypt.SALT,
n: Optional[int] = defaults.Scrypt.N,
r: Optional[int] = defaults.Scrypt.R,
p: Optional[int] = defaults.Scrypt.P,
dklen: Optional[int] = defaults.Scrypt.DKLEN) -> BaseKey:
"""
Function to create ``BaseKey``.
Uses the ``sha256(scrypt(...))``.
.. warning::
RAM consumption is calculated by ``128 * r * (n + p + 2)``.
Arguments:
phrase (``bytes``, ``Phrase``):
Passphrase from which
``BaseKey`` will be created.
salt (``bytes``, ``int``, optional):
Scrypt Salt.
n (``int``, optional):
Scrypt N.
r (``int``, optional):
Scrypt R.
p (``int``, optional):
Scrypt P.
dklen (``int``, optional):
Scrypt dklen.
"""
phrase = phrase.phrase if isinstance(phrase, Phrase) else phrase
if isinstance(salt, int):
bit_length = ((salt.bit_length() + 8) // 8)
length = (bit_length * 8 ) // 8
salt = int.to_bytes(salt, length, 'big')
maxmem = 128 * r * (n + p + 2)
scrypt_key = scrypt(
phrase, n=n, r=r, dklen=dklen,
p=p, salt=salt, maxmem=maxmem
)
return BaseKey(sha256(scrypt_key).digest())
[docs]
def make_mainkey(basekey: BaseKey, box_salt: BoxSalt) -> MainKey:
"""
Function to create ``MainKey``.
Arguments:
basekey (``bytes``):
Key which you received with scrypt
function or any other key you want.
box_salt (``BoxSalt``):
``BoxSalt`` generated on *LocalBox* creation.
"""
return MainKey(sha256(basekey + box_salt).digest())
[docs]
def make_filekey(key: Union[MainKey, DirectoryKey], file_salt: FileSalt) -> FileKey:
"""
Function to create ``FileKey``.
The ``FileKey`` is a ``Key`` that we use to encrypt the file
and its metadata (except ``efile_path``) on upload. Prior to
the version **1.3** to make a ``FileKey`` we used ``MainKey``
and the ``FileSalt``, which is randomly generated (on file
preparation) 32 bytes. Started from now, instead of the
``MainKey`` we will use the ``DirectoryKey``, but you can
still generate old *FileKey(s)* with ``MainKey``, it's
here only for backward compatibility and this is legacy.
``MainKey`` or ``DirectoryKey`` *can not* be restored
from the ``FileKey``, so it's safe-to-share.
The main benefit in using the ``DirectoryKey`` over
``MainKey`` is that in old versions you will need
to share each of files from your *Box* separately,
while now you can share the one ``DirectoryKey``
and *Requester* will be able to make all of the
*FileKeys* to range of files in Dir by himself.
You still can share files separately, though.
See docs if you want to learn more about the
*Keys hierarchy* structure & other things.
Arguments:
key (``MainKey`` (legacy), ``DirectoryKey``):
Key which will be used to make a ``FileKey``.
file_salt (``FileSalt``):
``FileSalt`` generated on file prepare.
"""
return FileKey(sha256(key + file_salt).digest())
[docs]
def make_hmackey(filekey: FileKey, file_salt: FileSalt) -> HMACKey:
"""
Function to create ``HMACKey``.
``HMACKey`` is a ``Key`` that is used exclusively
to derive a *HMAC* (by default *SHA256*) of a
*File* or any target bytestring.
Arguments:
filekey (``FileKey``):
``FileKey`` which will be used to make a ``HMACKey``.
file_salt (``FileSalt``):
``FileSalt`` that correspond to ``FileKey``.
"""
return HMACKey(HMAC(filekey.key, file_salt.salt, 'sha256').digest())
[docs]
def make_dirkey(mainkey: MainKey, part_id: bytes) -> DirectoryKey:
"""
Function to create ``DirectoryKey``.
``DirectoryKey`` is generated from the unique
path *PartID* and the ``MainKey``. We use the
``DirectoryKey`` to make a ``FileKey``. See
``help(tgbox.keys.DirectoryKey)`` and docs
for more information about this type of ``Key``.
"""
sha256_mainkey = sha256(mainkey.key).digest()
return DirectoryKey(sha256(sha256_mainkey + part_id).digest())
[docs]
def make_requestkey(key: Union[MainKey, BaseKey],
salt: Union[FileSalt, BoxSalt, bytes]) -> RequestKey:
"""
Function to create ``RequestKey``.
All files in *RemoteBox* is encrypted with filekeys, so
if you want to share (or import) file, then you need to
get ``FileKey``. For this purpose you can create ``RequestKey``.
Alice has file in her Box which she wants to share with Bob.
Then: A sends file to B. B forwards file to his Box, takes
``FileSalt`` from A File and ``MainKey`` of his Box and calls
``make_requestkey(key=mainkey, salt=file_salt)``.
``RequestKey`` is a compressed pubkey of ECDH on *SECP256K1* curve,
B makes privkey with ``sha256(mainkey + salt)`` & exports pubkey
to make a shared secret bytes (key, with which A will
encrypt her filekey/mainkey. The encrypted (file/main)key
is called ``ShareKey``. Use help on ``make_sharekey``.).
B sends received ``RequestKey`` to A. A makes ``ShareKey``
and sends it to B. B calls ``get_importkey`` and receives the
``ImportKey``, which is, in fact, a ``FileKey``.
No one except Alice and Bob will have ``FileKey``. If Alice want
to share entire Box (``MainKey``) with Bob, then Bob creates
slightly different ``RequestKey`` with same function:
``make_requestkey(key=mainkey, salt=box_salt)``.
Please note that ``FileKey`` can only decrypt a some
*RemoteBox* with which it is associated. However, if
Alice will want to share the entire *Directory* of
her *Box* files (i.e */home/alice/Pictures* folder)
then Bob can make a ``RequestKey`` to any file
from this *Directory*, and Alice will make a
``ShareKey`` with a ``DirectoryKey`` instead
of ``FileKey``. See help on ``make_sharekey``.
.. note::
Functions in this module is low-level, you can make ``RequestKey`` for
a forwarded from A file by calling ``get_requestkey(...)``
method on ``EncryptedRemoteBoxFile`` | ``EncryptedRemoteBox``.
Arguments:
key (``MainKey``, ``BaseKey``):
Bob's *Key*. If you want to import other's
*file*, then you need to specify here
``MainKey`` of your *LocalBox*, otherwise
specify ``BaseKey`` (to clone *RemoteBox*)
salt (``FileSalt``, ``BoxSalt``, ``bytes``):
Most obvious ``salt`` is Alice's ``BoxSalt`` or
``FileSalt``, however, ``salt`` here is just
some bytestring that will be hashed with the
``MainKey`` to make the output ECDH keys
unique, so you can specify here any bytes
value if you understand consequences (you
will need to reuse it on ``make_importkey``).
"""
if not any((isinstance(salt, Salt), isinstance(salt, bytes))):
raise ValueError('`salt` is not Union[Salt, bytes]')
if FAST_ENCRYPTION:
skey_data = int.from_bytes(sha256(key + salt).digest(), 'big')
skey = ec.derive_private_key(skey_data, ec.SECP256K1())
vkey = skey.public_key().public_bytes(
encoding=Encoding.X962,
format=PublicFormat.CompressedPoint)
else:
skey = SigningKey.from_string(
sha256(key + salt).digest(),
curve=SECP256k1, hashfunc=sha256
)
vkey = skey.get_verifying_key()
vkey = vkey.to_string('compressed')
return RequestKey(vkey)
[docs]
def make_sharekey(
key: Union[FileKey, MainKey, DirectoryKey],
salt: Optional[Union[FileSalt, BoxSalt, bytes]] = None,
requestkey: Optional[RequestKey] = None) \
-> Union[ShareKey, ImportKey]:
"""
Function to create ``ShareKey``.
.. note::
You may want to know what is ``RequestKey`` before reading
this. Please, run help on ``make_requestkey`` to get info.
Alice received ``RequestKey`` from Bob. But what she should do
next? As reqkey is just EC-pubkey, she wants to make a *shared
secret key*. A makes her own privkey, with ``sha256(mainkey
+ sha256(salt + requestkey))`` & initializes ECDH with B pubkey
and her privkey. After this, A makes a hashed with SHA256
*shared secret*, which will be used as 32-byte length AES-CBC
key & encrypts her *File|Main|Directory* key. IV here is first 16
bytes of the ``sha256(requestkey)``. After, she prepends her pubkey to
the resulted encrypted *File|Main|Directory* key and sends it to Bob.
With A pubkey, B can easily get the same shared secret and
decrypt ``ShareKey`` to make the ``ImportKey``.
The things will be much less complicated if Alice don't mind
to share her File, Dir or Box with ALL peoples. In this case
we don't even need to make a ``ShareKey``, ``ImportKey``
will be returned from the raw target ``Key``.
Arguments:
key (``MainKey``, ``FileKey``, ``DirectoryKey``):
o If ``key`` is instance of ``MainKey``: Box key.
Specify only this kwarg and ignore ``requestkey``
if you want to share your Box with **ALL** peoples.
Your Box ``key`` -- ``MainKey`` will be NOT encrypted.
o If ``key`` is instance of ``FileKey``: File key.
Specify only this kwarg if you want to share your
File with **ALL** peoples. **No encryption** if
``RequestKey`` (as ``requestkey``) is not specified.
o If ``key`` is instance of ``DirectoryKey``: Dir key.
Specify only this kwarg if you want to share your
File with **ALL** peoples. **No encryption** if
``RequestKey`` (as ``requestkey``) is not specified.
salt (``FileSalt``, ``BoxSalt``, ``bytes``, optional):
Most obvious ``salt`` is Alice's ``BoxSalt`` or
``FileSalt``, however, ``salt`` here is just
some bytestring that will be hashed with the
``MainKey`` to make the output ECDH keys
unique, so you can specify here any bytes
value if you understand consequences. For
example, we will use PartID (``bytes``) as
salt on ``DirectoryKey`` sharing.
requestkey (``RequestKey``, optional):
``RequestKey`` of Bob. With this must be
specified ``salt``.
"""
if not all((requestkey, salt)):
return ImportKey(key.key)
skey_salt = sha256(salt + requestkey.key).digest()
if FAST_ENCRYPTION:
skey_data = int.from_bytes(sha256(key + skey_salt).digest(), 'big')
skey = ec.derive_private_key(skey_data, ec.SECP256K1())
vkey = skey.public_key().public_bytes(
encoding=Encoding.X962,
format=PublicFormat.CompressedPoint
)
b_pubkey = ec.EllipticCurvePublicKey.from_encoded_point(
curve=ec.SECP256K1(),
data=requestkey.key
)
enc_key = skey.exchange(
algorithm=ec.ECDH(),
peer_public_key=b_pubkey)
else:
skey = SigningKey.from_string(
sha256(key + skey_salt).digest(),
curve=SECP256k1, hashfunc=sha256
)
vkey = skey.get_verifying_key()
vkey = vkey.to_string('compressed')
b_pubkey = VerifyingKey.from_string(
requestkey.key, curve=SECP256k1
)
ecdh = ECDH(
curve=SECP256k1,
private_key=skey,
public_key=b_pubkey
)
enc_key = ecdh.generate_sharedsecret_bytes()
enc_key = sha256(enc_key).digest()
iv = sha256(requestkey.key).digest()[:16]
encrypted_key = AES(enc_key, iv).encrypt(
key.key, pad=False, concat_iv=False
)
return ShareKey(encrypted_key + vkey)
[docs]
def make_importkey(
key: Union[MainKey, BaseKey], sharekey: ShareKey,
salt: Optional[Union[FileSalt, BoxSalt, bytes]] = None) -> ImportKey:
"""
.. note::
You may want to know what is ``RequestKey`` and
``ShareKey`` before using this. Use ``help()`` on
another ``Key`` *make* functions.
``ShareKey`` is a combination of encrypted by Alice
(File/Main/Directory)Key and her pubkey. As Bob can create
again ``RequestKey``, which is PubKey of ECDH from
``sha256(key + salt)`` PrivKey, and already have
PubKey of A, -- B can create a shared secret, and
decrypt A ``ShareKey`` to make an ``ImportKey``.
Arguments:
key (``MainKey``, ``BaseKey``):
Bob's ``MainKey`` or ``BaseKey`` that
was used on ``RequestKey`` creation.
sharekey (``ShareKey``):
Alice's ``ShareKey``.
salt (``FileSalt``, ``BoxSalt``, ``bytes``, optional):
Salt that was used on ``RequestKey`` creation.
"""
if len(sharekey) == 32: # Key isn't encrypted.
return ImportKey(sharekey.key)
if not salt:
raise ValueError('`salt` must be specified.')
requestkey = make_requestkey(key, salt)
if FAST_ENCRYPTION:
skey_data = int.from_bytes(sha256(key + salt).digest(), 'big')
skey = ec.derive_private_key(skey_data, ec.SECP256K1())
a_pubkey = ec.EllipticCurvePublicKey.from_encoded_point(
curve=ec.SECP256K1(),
data=sharekey[32:]
)
dec_key = skey.exchange(
algorithm=ec.ECDH(),
peer_public_key=a_pubkey)
else:
skey = SigningKey.from_string(
sha256(key + salt).digest(),
curve=SECP256k1, hashfunc=sha256
)
a_pubkey = VerifyingKey.from_string(
sharekey[32:], curve=SECP256k1
)
ecdh = ECDH(
curve=SECP256k1,
private_key=skey,
public_key=a_pubkey
)
dec_key = ecdh.generate_sharedsecret_bytes()
dec_key = sha256(dec_key).digest()
iv = sha256(requestkey.key).digest()[:16]
decrypted_key = AES(dec_key, iv).decrypt(
sharekey[:32], unpad=False
)
return ImportKey(decrypted_key)