Protocol#

The TGBOX Protocol is a number of rules and algorithms that define how all of things (like Encryption, packing Metadata, File sharing & etc) work. As TGBOX is built around the Telegram messenger, we can call a TGBOX as an additional layer which adds some new features.

Algorithms used in Encryption#

  • For encryption, we use the AES CBC with 256 bit key. First 16 bytes of any encrypted by library data is IV;

  • For making the BaseKey (a key that is used to derive all sub-keys) we use by default Scrypt PBKDF;

  • As hash function (for various purposes) we always use the SHA256;

  • For File and Box Sharing we propose and use by default ECDH on SECP256k1 curve;

  • Started from version 1.5 we use HMAC-SHA256 to verify that any bit of plaintext was not tweaked.

Abstract Box#

Note

More detailed in RemoteBox and LocalBox

  • The Box is an object that have BoxSalt — 32 (usually random) bytes. With this Salt and user Passphrase we make main encryption key (see Encryption keys hierarchy).

  • The Box splits into two types, — the Remote (is a Telegram Channel) and Local (is a SQLite database). They have a two states, — the Encrypted (when Passphrase is not presented) and Decrypted.

  • The RemoteBox store encrypted Files and their Metadata. LocalBox store Metadata and Directories (see details in LocalBox).

  • The LocalBox can be fully restored from the RemoteBox if you have a decryption key (but this can take a little of time if you uploaded a big amount of files);

  • The Box can be shared with multiple users.

Abstract Box file#

Note

More details in RemoteBox and LocalBox

  • An abstract Box file is an object that have FileSalt — 32 random bytes. With this Salt and user Passphrase -> BaseKey -> MainKey we used to make a file encryption key. Started from the version 1.3 we make a file encryption key with the DirectoryKey and FileSalt. See details in Encryption keys hierarchy;

  • The Box file has Metadata (see Box file & its Metadata);

  • The Box file splits into two types, — the Remote (stored in a RemoteBox) and Local (metadata of the Remote file stored in a LocalBox). They have a two states, — the Encrypted (when FileKey is not presented) and Decrypted;

  • The Box file can be shared with multiple users without giving key of the whole Box; the Requester will only have an access to requested file, and nothing more.

Encryption keys hierarchy#

_images/keys_hierarchy.png

The Phrase#

Phrase is a User’s password or generated by Protocol API six random mnemonic words. There is a special class in the TGBOX that can make a phrase: tgbox.keys.Phrase. The Phrase is used to only create a BaseKey.

The BaseKey#

BaseKey is a master Key that is used to derive all other sub-keys. By default, we make this Key with a tgbox.keys.make_basekey() function, which utilize the Scrypt KDF under the hood and then hash result with SHA256. The Scrypt is configured to require a 1GB of RAM to make a key, and uses non unique salt: tgbox.defaults.Scrypt. Experienced users may want to change it to make a brute-force attack impossible, but should not lost it (we do not store it in any way). Random Phrase or secure password should be just enough to protect your Box. You can wrap any other key in the BaseKey class if you want a different implementation.

We also use BaseKey to encrypt Telegram session (give an access to the Account) in the LocalBox.

The MainKey#

Note

You mostly will not need to use this and all next Keys directly, because it’s a Protocol business.

MainKey is a Key that is used to derive a directory keys and to encrypt some of the LocalBox data. When we start a “Box making” routine by firstly calling a make_remotebox() function (and then make_localbox()), we receive a 32 random bytes, – the BoxSalt. By concatenating and then hashing by SHA256 the MainKey with BoxSalt (tgbox.keys.make_mainkey()) we make a MainKey.

We also use MainKey to encrypt some of the data stored in RemoteBox file Metadata.

The DirectoryKey#

DirectoryKey is a Key that is used to derive a file keys. You may want to read the “How does we store file paths” in LocalBox firstly to understand it more clearly. In short, every File in the TGBOX (just as in any OS) has a file path. Every unique (case-sensitive) file path has it’s own DirectoryKey, and an every Part of the file path has it’s own ID, that is linked with the parent Part ID. To make a DirectoryKey, we need to have a Head Part ID (ID of last path part) and MainKey. Firstly, we hash a MainKey, then concate hashed MainKey with the Head Part ID, then hash it again. The final result is a DirectoryKey. See make_dirkey() source code.

In fact, the DirectoryKey is more a deterministic bytes than a Key. It doesn’t encrypt anything, but used only to make a file keys.

The FileKey#

FileKey is a Key that is used to encrypt file and its Metadata. On prepare_file() we receive a 32 random bytes, – the FileSalt. Just identical to make_mainkey(), we make a FileKey with make_filekey(). Started from the version 1.3, to derive a file keys we use a DirectoryKey. For files that was uploaded prior to the v1.3, we use a MainKey.

The HMACKey#

HMACKey is a Key that is used from v1.5 to compute a HMAC of a file (plaintext) on upload. We make a HMACKey with make_hmackey(), which under the hood utilize the hmac.HMAC on SHA256 with a FileKey and FileSalt as message to derive a HMACKey.

Box file & its Metadata#

On “uploading some file to the Box” routine, the target firstly goes through the prepare_file() function. In it, we verify that file is valid and if it is, we construct the Box file Metadata, which consist of the next fields:

  • file_salt (bytes: required, public)FileSalt is used for FileKey creation

  • box_salt (bytes: required, public)BoxSalt is used for MainKey creation

  • file_fingerprint (bytes: v1.1+, public)A SHA256 of the File’s path (with name) plus MainKey

  • efile_path (bytes: v1.3+, public)Encrypted (by MainKey) File’s path

  • minor_version (int: v1.3+, public)The minor version of the TGBOX protocol

  • _BFP (bytes: v1.5+, required, secret)5 random bytes for IV Bit-flipping protection

  • file_name (bytes: required, secret)File’s name

  • file_size (int: required, secret)Pure file’s size, no metadata included

  • duration (float: optional, FFMPEG required, secret)File’s duration (if video/audio)

  • cattrs (bytes: optional, secret)File’s custom attributes specified by User

  • has_hmac_sha256 (bytes: v1.5+, required, secret)Signal that file has HMAC checksum

  • preview (bytes: optional, FFMPEG required, secret)File’s preview (if file is media)

  • mime (bytes: required, secret)File’s mime type

Unpacked Metadata also have some fixed bytes at the beginning, which consist of the:
  • prefixBytes (6) to identify the TGBOX encrypted file

  • verbyteProtocol global version as one byte

  • metadata_sizeBytesize (3) of the Metadata to unpack

Packing Algorithm#

To pack a Key-Value container we use the simple algorithm, that in Protocol is called a PackedAttributes. The packed result here is combination of Key length plus Key plus Value length plus Value (all values should be bytes) and so on. We store the Key/Value length in three bytes, so the maximum bytelength for each Key or Value is 16MiB-1.

_images/pattrs.png

In the upper image example, FF (is hexed [int 255], as well as Key length & Value length) is a Magic number that identify a PackedAttributes bytestring. The 000005 is a Key length, the next is a Key, which is “field”. So, we slice the first three bytes after Magic number, get a Key length, then we slice a Key length, get a Key. After Key there should be the next three bytes that represent a Value length. We make the same operation as with Key and receive a Value, which is “data”. Repeat this until packed string is not empty.

from tgbox.tools import PackedAttributes

pattrs = PackedAttributes.pack(field=b'data', x=b'test')
# b'\xff\x00\x00\x05field\x00\x00\x04data\x00\x00\x01x\x00\x00\x04test'

print(PackedAttributes.unpack(pattrs))
# {'field': b'data', 'x': b'test'}

Changed in version v1.5: (1) Now positions of Key/Value in PackedAttributes output bytestring is always randomized (random_seed kwarg, default is urandom(32)). (2) The keys, specified in protected_keys tuple kwarg will never be at the start or at the end of packed string (optional).

Bit-flipping Protection#

Started from the version 1.5 we implement some basic protection against the attacks that involve file modification. While it can be done only by Telegram server maintainers & peoples who have enough privileges to edit your files in RemoteBox, I believe that it would be nice addition to security stack of the TGBOX protocol.

Note

Prior to v1.5 we didn’t have any protection against Bit-flipping because TGBOX isn’t a network protocol, it’s a something like file-storage provider, where it is not necessary. However, it’s nice-to-have thing, so here we go!

Secret Metadata protection#

Important

Why bother? Started from version 1.5, Secret Metadata will store the new has_hmac_sha256 key, which would signal Protocol that we must check HMAC of this file on download(). Attacker potentially can Bit-flip some bytes in Secret Metadata to disable this check.

Below is additions that should prevent such scenario.

Secret Metadata is a part of Metadata, fields of which is packed with PackedAttributes and encrypted with FileKey. We can’t just add HMAC-SHA256 to it, as it will break the backward-compatibility. Instead, we will use different approach

  1. In short, changing any bit in block of encrypted data will result in garbaging the whole block after decryption (see Bit-flipping attack). As you can see in Packing Algorithm, packed string contains information bytes(3) that help us separate the Key from Value. Garbaging any of this bytes will result in incorrect unpacking & error;

  2. To prevent attacker from understanding the structure of packed bytestring and higher up chances of garbaging Secret Metadata in controlled way, Key/Value order of dict is additionally randomized on packing process, as stated in Packing Algorithm;

  3. To protect first block of encrypted Secret Metadata from Bit-flipping with IV (flipping IV would not result in garbage after decryption), first Key of packed Secret Metadata will be always _BFP with five pseudo-random bytes. Total of \xff (magic number, 1), \x00\x00\x04 (key size, 3), _BFP (key, 4), \x00\x00\x05 (value size, 3), \x00\x00\x00\x00\x00 (i.e some 5 pseudo-random bytes, 5) will form 16 bytes, or whole first CBC block. Changing this block will make zero sense or will lead to unpacking error;

  4. We will add has_hmac_sha256 to protected_keys tuple kwarg, which will ensure that this key will never be after _BFP or never be last Key/Value in Secret Metadata. All Keys/Values in this part of Metadata are required, meaning that even one lost Key/Value (except has_hmac_sha256, which we protect here) will result in invalid unpacking;

  5. Attacker will never know result of Bit-flipping before You download file and tell them.

All this things (with more little checks in the code) should ensure that has_hmac_sha256 will not be changed.

Encrypted File protection#

Started from the version 1.5 we will protect original file with HMAC-SHA256 on upload process. On download routine, we will verify this checksum if has_hmac_sha256 present in Secret Metadata or if minor_version >= 5. We compute HMAC-SHA256 over plaintext (E&M) and append it to the end of encrypted file.

Note

Last 32 bytes of encrypted v1.5+ file is always HMAC.

Why E&M while we can use EtM? While EtM makes sense in networking protocols, here in TGBOX we operate over big chunks of bytes (up to 4GB). It will be strange if firstly we would download whole encrypted file to RAM (or disk), then check HMAC, then decrypt & save. Either we will compute HMAC of encrypted bytedata chunk-by-chunk on decryption (which doesn’t make sense, we’re already decrypting), or this will be bizarre overhead. Instead, we make HMAC over plaintext. First of all, it will guarantee file authenticity (any change in encrypted bytes will affect decrypted). Secondly, this approach will allow us to verify a partially downloaded file. For example, if our download process was interrupted for some reason, we have ability to continue fetching bytes from specified offset. With EtM we would need to Update HMAC with all previously downloaded encrypted bytes to restore HMAC state. But with E&M we can easily calculate over already downloaded decrypted bytes.

Note

Every unique Box file has unique HMACKey, thus no correlations about file contents can be made.

Metadata in depth#

_images/rbfm_schema.png
On this schema:
  • Only Metadata keys shown;

  • The efile_path field is encrypted with MainKey. It is now a part of public Metadata, so we can decrypt it, make a DirectoryKey and then FileKey;

  • The secret_metadata field is encrypted with FileKey.

Note

Metadata is always placed at the start of the Box file.

Describing in Code#

This code example will decrypt and parse example file that was uploaded in my public Box with disclosed MainKey. If you want to test a file from your Box, then you will need to make a MainKey.

# = Decrypt File Metadata ============================================ #

import pathlib, tgbox

# The MainKey of the example Box is already disclosed, see t.me/nontgbox_non
MAINKEY = tgbox.keys.Key.decode('MbxTyN4T2hzq4sb90YSfWB4uFtL03aIJjiITNUyTqdoU=')
# You need to download the encrypted example Box file: t.me/nontgbox_non/90
BOXFILE = open('awtgMbMtFpEvxXdwrRMruZeJEJEbwk28QyeYr6oaEWM=','rb').read()

# There is PREFIX, VERBYTE and METADATA_SIZE which is always
# fixed in the first 10 bytes of the encrypted by Protocol file
FIXED_METADATA = BOXFILE[:10] # b'\x00TGBOX\x01\x00\x01\x9d'

PREFIX = FIXED_METADATA[:6] # b'\x00TGBOX' (is signature)
VERBYTE = FIXED_METADATA[6:7] # b'\x01' (major Protocol version)

METADATA_SIZE = FIXED_METADATA[7:] # b'\x00\x01\x9d' (size of the Metadata)
# Convert the bytes METADATA_SIZE to the integer type
METADATA_SIZE = tgbox.tools.bytes_to_int(METADATA_SIZE) # 413

# Actual Metadata goes after Fixed, so slice from 10 to METADATA_SIZE+10 (Fixed Metadata bytesize)
METADATA = BOXFILE[10:METADATA_SIZE+10] # b'\xff\x00\x00\x0fsecret_metadata\x00\x00...>
UNPACKED_METADATA = tgbox.tools.PackedAttributes.unpack(METADATA) # {'secret_metadata': b'\x08\xff\xfa<\x04...>

# To decrypt the Secret Metadata we need to make a DirectoryKey, and
# then the FileKey, so firstly we will decrypt the efile_path and
# make a DirectoryKey from the last Path Part ID
file_path = tgbox.crypto.AESwState(MAINKEY).decrypt(UNPACKED_METADATA['efile_path'])
file_path = pathlib.Path(file_path.decode()) # '/home/tgbox/v1.5', ppart_id_generator require Path object

for path_part in tgbox.tools.ppart_id_generator(file_path, MAINKEY):
    part_id = path_part[2] # ppart_id_generator yields tuple

# Started from v1.3 we make FileKeys from DirectoryKey, not MainKey
dirkey = tgbox.keys.make_dirkey(MAINKEY, part_id)
# We make a FileKey from DirectoryKey and FileSalt (always in public Metadata)
filekey = tgbox.keys.make_filekey(dirkey, UNPACKED_METADATA['file_salt'])

secret_metadata = tgbox.crypto.AESwState(filekey).decrypt(UNPACKED_METADATA['secret_metadata']) # b'\xff\x00\x00\x04_BFP...>
secret_metadata = tgbox.tools.PackedAttributes.unpack(secret_metadata) # {'_BFP': b'i\x95\xe0\xc1\x9b'...>

print(secret_metadata) # {'_BFP': b'i\x95\xe0\xc1\x9b', 'file_name': b'doc...>

Tip

The next code blocks can be inserted in the end of the code above

# = Decrypt actual File ============================================ #

from hmac import HMAC, compare_digest # v1.5+ files require HMAC

# Actual encrypted File (original file that was uploaded by user)
# position is FIXED_METADATA size (10, -- PREFIX + VERBYTE +
# METADATA_SIZE) plus METADATA_SIZE (integer)
encrypted_file_pos = 10 + METADATA_SIZE # 423

# encrypted_file includes IV as first 16 bytes
encrypted_file = BOXFILE[encrypted_file_pos:]

# Last 32 bytes of encrypted file is HMAC-SHA256 if
# 'secret_metadata' has 'has_hmac_sha256' Key (v1.5+)
if secret_metadata.get('has_hmac_sha256', None):
    hmac_sha256 = encrypted_file[-32:]
    encrypted_file = encrypted_file[:-32]

    # We need to verify HMAC if File has it,
    # we do so with Plaintext and HMACKey
    hmackey = tgbox.keys.make_hmackey(filekey=filekey,
        file_salt = tgbox.crypto.FileSalt(file_salt))
else:
    hmac_sha256 = None

# Just similar to Secret Metadata, we decrypt File with FileKey
decrypted_file = tgbox.crypto.AESwState(filekey).decrypt(encrypted_file)

if hmac_sha256:
    hmac_state = HMAC(hmackey.key, decrypted_file, digestmod='sha256')
    print(compare_digest(hmac_sha256, hmac_state.digest())) # True

# I made & uploaded an example text File, so we can print it
print(decrypted_file) # b'This file will be deconstructed in v1.5 docs! :)\n...>

File Storage#

When user “adds some file to the Box”, we:

  1. Check it for validity, make Metadata and store it in PreparedFile object;

  2. Take PreparedFile, concatenate Metadata with encrypted File and upload it to the RemoteBox;

  3. Store Metadata plus File IV alongside with other data in the SQLite Database (or the LocalBox).

We store user’s Box file (Metadata plus Encrypted user File) in the RemoteBox. Locally, in the LocalBox, we store only Metadata (and some other data that can help us to operate faster on local storage). You may refer to LocalBox as “RemoteBox cache”. It’s always better to use Local for gathering info about Files.

Updating Files#

Although the Telegram messenger doesn’t allow us to update a parts of already uploaded Files, there is some methods in the Protocol that can help you in some scenarios.

Updating Metadata#

You can update some Metadata attributes of the Box File after it was uploaded. For example, you can change a File name of File path (last will change Directory too, like “move to folder” operation) with update_metadata() on LocalBox File with corresponding DecryptedRemoteBoxFile as drbf argument. Please note that we can not partially update already uploaded to Telegram File, so your updated Metadata attributes will be stored in encrypted and encoded form in the File caption, which have it’s own limits (~2KB/~4KB Premium).

Re-uploading File#

You can fully re-upload (and so edit) already existen Box File. This can be useful for small files that constantly change its contents. To do so, you should prepare a new file with prepare_file(), get a DecryptedRemoteBoxFile that you want to change and call update_file() on DecryptedRemoteBox. No interaction with LocalBox is needed, as tgbox.api.utils.PreparedFile contains DecryptedLocalBox as object and will be updated automatically.

Sharing#

The Protocol fully supports File or Box sharing with other people.

Sharing Box file#

To share a Box file we need to extract a FileKey and send it to Requester alongside with the Box file itself. We can share a Key as-is, but this is not secure. Instead, we will establish a secure canal via asymmetric cryptography (with ECDH).

  1. Alice forwards file from her RemoteBox Channel to Bob;

  2. Bob forwards received file to his RemoteBox Channel;

  3. B gets EncryptedRemoteBoxFile and calls get_requestkey() on it, obtains RequestKey;

  4. A receives RequestKey from B (can be shared via insecure canals);

  5. A makes ShareKey with B’s RequestKey and sends it to B (can be shared via insecure canals);

  6. B makes ImportKey with A’s ShareKey, decrypts EncryptedRemoteBoxFile and imports it.

In more low-level#

  • 0. Bob makes his own Box

    To import other’s Box file, Bob will need to create his own Box

  • 1. Alice forwards Box file to Bob

    Alice will need to forward Box file from her RemoteBox to chat with Bob. This can be done within Telegram or by using a TelegramClient object from the *RemoteBox (see tc)

  • 2. Bob forwards Box file to his Box

    Bob will need to forward Box file from chat with Alice to his RemoteBox. Then, obtain it as EncryptedRemoteBoxFile (i.e. with get_file() or with search_file())

  • 3. Bob makes RequestKey

    After obtaining a EncryptedRemoteBoxFile of Alice as object, Bob will need to call a get_requestkey() method on it, which will result in RequestKey. Under the hood, this method will call a make_requestkey() function. In it, Bob will make a EC private key on SECP256k1 from the sha256(b_mainkey + file_salt), and corresponding public key of private key in a compressed form is a RequestKey.

  • 4. Alice receives RequestKey from Bob

    Can be done within Telegram, TGBOX (tc) or any other insecure communication canal.

  • 5. Alice makes ShareKey with RequestKey of Bob and sends it

    1. A creates her EC private key on SECP256k1 from the sha256(a_mainkey + sha256(file_salt + b_requestkey)). After this, she will extract public key from B’s RequestKey and will make a shared 32 byte-secret with ECDH(a_privkey, b_pubkey{requestkey}, secp256k1). Shared secret is additionally hashed with SHA256. Hashed shared secret is AES-CBC key for FileKey encryption;

    2. A make SHA256 hash from B’s RequestKey and take first 16 bytes from result, this is AES-CBC IV.

    3. A encrypts her FileKey with hashed shared secret and IV. Let’s call result as eFileKey. After this she constructs ShareKey as follows: ShareKey(efilekey + a_pubkey). We don’t concat IV to the ShareKey because B can extract it from his RequestKey.

  • 6. Bob makes ImportKey with ShareKey of Alice & decrypts File

    Bob will repeat a process of making a RequestKey in the make_importkey() function. ShareKey consist of encrypted filekey and public key of Alice. On making RequestKey Bob will make his own private key, so he can easily get the same shared secret with ECDH as Alice and decrypt the encrypted filekey to obtain ImportKey (which is actually a FileKey). ECDH(b_privkey, a_pubkey{requestkey}, secp256k1) -> SHA256 -> {decrypt efilekey}

  • 7. Bob imports decrypted EncryptedRemoteBoxFile

    Bob will call decrypt() with resulted ImportKey from step 6. and receive DecryptedRemoteBoxFile(). To store information about this file in the LocalBox, Bob will need to call import_file() method. This will store a FileKey in encrypted form in LocalBox of Bob so he can always access imported file of Alice.

Note

We store file keys of other people only in your LocalBox. You will easily retrieve all information about your files in Remote if corresponding to it Local will be lost (you can always make a LocalBox from RemoteBox), however, it’s will be impossible to decrypt all imported files, and you will need to request a keys for them again.

Sharing Box directory#

Note

This chapter is only for files that was uploaded from version 1.3+!

Warning

By sharing DirectoryKey with someone you also give a full access to all of the files that will be uploaded to corresponding Directory in future! Make sure you understand this before using proposed method from below! Use File sharing to share files individually.

If you want to share many files at once, with previous approach (Sharing Box file) you will be need to make a separate ShareKey for each target file. Started from version 1.3 now it’s possible to share a bunch of files per one request. To do so, target Box files should be linked to one abstract file path in your Box. For example, you can share all files that have a /home/user/Pictures path (we call it Directory) with only one ShareKey.

Tip

More details about implementation of abstract Directories you can find in LocalBox.

To share a Box directory we need to extract a DirectoryKey and send it to Requester alongside with the Box files from target Directory. We can share a Key as-is, but this is not secure. Instead, we will establish a secure canal via asymmetric cryptography (with ECDH).

  1. Alice forwards files from Directory from her RemoteBox Channel to Bob;

  2. Bob forwards received files to his RemoteBox Channel;

  3. B gets last received EncryptedRemoteBoxFile and calls get_requestkey() on it, obtains RequestKey;

  4. A receives RequestKey from B (can be shared via insecure canals);

  5. A makes ShareKey of DirectoryKey with B’s RequestKey and sends it to B (can be shared via insecure canals);

  6. B makes ImportKey with A’s ShareKey, decrypts each received EncryptedRemoteBoxFile and imports them.

In more low-level#

  • 0. Bob makes his own Box

    To import other’s Box file, Bob will need to create his own Box

  • 1. Alice forwards Box files of Directory to Bob

    To get each file that is linked to some Directory, Alice can use a search_file() with SearchFilter that contains a file_path. Most efficiently, Alice can get a DecryptedLocalBoxDirectory object with get_directory() and iterate over it via iterdir(). While iterating, Alice can use a forward_messages() method from RemoteBox Telegram Client (tc()).

  • 2. Bob forwards Box files of Directory to his Box

    This step can be omitted if in 1. Alice will forward directly to the Box of Bob. To do so, Bob should add Alice to his RemoteBox Channel and grant her with permission to Post messages. Otherwise, Bob will need to forward each file from Alice within Telegram or find a way to do it programmatically with tc.

  • 3. Bob makes RequestKey to last file

    After forwarding every EncryptedRemoteBoxFile from Alice, Bob should obtain last forwarded Box file as object and call a get_requestkey() method on it, which will result in RequestKey. Under the hood, this method will call a make_requestkey() function. In it, Bob will make a EC private key on SECP256k1 from the sha256(b_mainkey + file_salt), and corresponding public key of private key in a compressed form is a RequestKey.

  • 4. Alice receives RequestKey from Bob

    Can be done within Telegram, TGBOX (tc) or any other insecure communication canal.

  • 5. Alice makes ShareKey with RequestKey of Bob and sends it

    1. A creates her EC private key on SECP256k1 from the sha256(a_mainkey + sha256(file_salt{of last file} + b_requestkey)). After this, she will extract public key from B’s RequestKey and will make a shared 32 byte-secret with ECDH(a_privkey, b_pubkey{requestkey}, secp256k1). Shared secret is additionally hashed with SHA256. Hashed shared secret is AES-CBC key for DirectoryKey encryption. DirectoryKey can be easily obtained from dirkey;

    2. A make SHA256 hash from B’s RequestKey and take first 16 bytes from result, this is AES-CBC IV.

    3. A encrypts her DirectoryKey with hashed shared secret and IV. Let’s call result as eDirectoryKey. After this she constructs ShareKey as follows: ShareKey(e_directorykey + a_pubkey). We don’t concat IV to the ShareKey because B can extract it from his RequestKey.

  • 6. Bob makes ImportKey with ShareKey of Alice & decrypts Files

    Bob will repeat a process of making a RequestKey in the make_importkey() function. ShareKey consist of encrypted filekey and public key of Alice. On making RequestKey Bob will make his own private key, so he can easily get the same shared secret with ECDH as Alice and decrypt the encrypted directorykey to obtain ImportKey (which is actually a DirectoryKey). ECDH(b_privkey, a_pubkey{requestkey}, secp256k1) -> SHA256 -> {decrypt edirectorykey}

  • 7. Bob imports decrypted EncryptedRemoteBoxFile

    Bob will call decrypt() with resulted ImportKey from step 6. on each file of Alice and will receive DecryptedRemoteBoxFile() objects. To store information about this files in the LocalBox, Bob will need to call import_file() method on each decrypted file. This will make & store a FileKey in encrypted form in LocalBox of Bob so he can always access imported files of Alice.

Sharing Box#

To share a Box we need to extract a MainKey and send it to Requester alongside with adding Requester to Box Channel. We can share a Key as-is, but this is not secure. Instead, we will establish a secure canal via asymmetric cryptography (with ECDH).

  1. Alice make & send invite link of her RemoteBox Channel to Bob;

  2. Bob joins RemoteBox Channel of Alice;

  3. B gets EncryptedRemoteBox object and calls get_requestkey() on it, obtains RequestKey;

  4. A receives RequestKey from B (can be shared via insecure canals);

  5. A makes ShareKey with B’s RequestKey and sends it to B (can be shared via insecure canals);

  6. B makes ImportKey with A’s ShareKey, decrypts EncryptedRemoteBox and clones it.

In more low-level#

  • 0. Bob makes BaseKey

    To clone RemoteBox of Alice, Bob firstly should create BaseKey for it. Proposed way is to use a make_basekey() function.

  • 1. Alice invites Bob to her RemoteBox channel

    Can be done within Telegram or TGBOX (tc).

  • 2. Bob gets EncryptedRemoteBox and calls get_requestkey on it

    Every RemoteBox has BoxSalt. The RemoteBox store it in Channel description, encoded by Url Safe Base64. From concated BoxSalt with new BaseKey of B we make a SHA256 hash. This hash acts as private key for ECDH on secp256k1 curve. We create public key from this private key, compress it, and return (by get_requestkey()) RequestKey(compressed_pubkey). Generally, RequestKey is a compressed EC public key.

  • 3. Alice receives RequestKey from Bob

    Can be done within Telegram or TGBOX (tc).

  • 4. Alice makes ShareKey with Bob’s RequestKey and sends it

    1. A creates her own private key similarly to B, with the difference only in the Salt. While B makes a private key and then public key (= RequestKey) from the BaseKey concated with the BoxSalt, Alice makes private key from the sha256(a_mainkey + sha256(box_salt + b_requestkey)). After this, she extracts public key from RequestKey of B and makes a shared 32 byte-secret with ECDH(a_privkey, b_pubkey{requestkey}, secp256k1). Shared secret is additionally hashed with SHA256. Hashed shared secret is AES-CBC key for MainKey encryption;

    2. A make SHA256 hash from B’s RequestKey and take first 16 bytes from result, this is AES-CBC IV.

    3. A encrypts her MainKey with hashed shared secret and IV. Let’s call result as eMainKey. After this she constructs ShareKey as follows: ShareKey(emainkey + a_pubkey). We don’t concat IV to the ShareKey because B can extract it from his RequestKey.

  • 5. Bob makes ImportKey with ShareKey of Alice & decrypts RemoteBox

    Bob will repeat a process of making a RequestKey in the make_importkey() function. ShareKey consist of encrypted mainkey and public key of Alice. On making RequestKey Bob will make his own private key, so he can easily get the same shared secret with ECDH as Alice and decrypt the encrypted mainkey to obtain ImportKey (which is actually a MainKey). ECDH(b_privkey, a_pubkey{requestkey}, secp256k1) -> SHA256 -> {decrypt emainkey}

  • 6. Bob imports decrypted EncryptedRemoteBoxFile

    Bob will call decrypt() with resulted ImportKey from step 5. and receive DecryptedRemoteBox(). To save this Key and all information about RemoteBox, Bob will need to clone it by using tgbox.api.remote.clone_remotebox() function, which result is DecryptedLocalBox. Now Bob can access and download any file that was uploaded by Alice, and also make its own uploads (if permission is granted).

Note

Taking into account our example with Alice and Bob, Alice will also need to grant Bob with administrator privileges (at least Post messages) in her RemoteBox Channel so he can upload its own files. Sure, if Alice will want to.

Warning

  • By sharing a whole Box (a MainKey of it) you grant a full read-only permission to Requester. Never do this if you don’t trust Requester!

  • There is plenty of inbuilt in Telegram Channel permissions for every unique Admin. You can allow to Upload(/Post) but forbid to Edit/Delete files of other’s.

Synchronization#

Taking into account our example in Sharing Box, if any of the Box user will push new files to RemoteBox, then other Box users should synchronize it. For example, if Alice will grant Admin privileges to Bob and he will push own file to Remote, the LocalBox of Alice will not know about this. To update a LocalBox with new information, Alice will use a sync() method of DecryptedLocalBox.

Fast Synchronization#

Fast Synchronization is a process in which we check the Telegram Channel Admin Log for Box updates (stored within 48 hours). It’s fast, efficient and default for sync(). It’s available for Admins only (Admin with Zero Rights will have access to Admin Log). As number of Admins per Telegram Channel is limited, some will prefer to use the Deep Synchronization.

Changed in version v1.7: Starting from version 1.7, Fast Sync must be enabled on Box with .defaults.change() on DecryptedLocalBox.

# Enable Fast Sync on your Box, dlb is DecryptedLocalBox
await dlb.defaults.change('FAST_SYNC_ENABLED', 1)

Deep Synchronization#

Deep Synchronization is a process in which we iterate over RemoteBox and compare each file with those from LocalBox. If some File is presented in Remote but not presented in Local, – it will be imported. If some File is presented in Local but not presented in Remote, then it will be removed from Local. The Deep Sync is pretty fast even with Box that have a thousands of Files, however, may make a many of unnecessary requests. In contrary to Fast Sync, the Deep Sync is not limited in 48 hours time span. Moreover, you can set a Start From ID parameter if you need to sync only most recent Files. Deep Sync can be enabled by the deep=True flag in the sync() method.

Note

Both Sync methods catch File (and Metadata) updates, import new Files & remove Files that no more exist in the RemoteBox. You can specify a Progress Callback and track progress of Sync process.

Versioning#

The TGBOX will try to follow the well known Semantic Versioning. Development cycle:

  1. We will increment Minor Version and push all updates to the default indev branch

  2. While developing, we will increment the alpha/beta tags of Version and make pre-release

  3. When all updates will be committed & tested, we will make a branch of Version

In future, we will push patches to the Version branch and make release of it.

  • You can get a version from tgbox.version module, and Minor Version as integer from tgbox.defaults.MINOR_VERSION constant.

  • The VERBYTE define compatibility, it is the major version. While it’s not incremented, all new updates MUST support previous file formats, methods, etc. Except Version byte there can be lower versions, like v1.1, v1.1.1, etc. Verbyte= b'\x00' and Verbyte= b'\x01' shouldn’t be compatible, otherwise we can use a lower version (minor/patch), i.e v1.1. Typically we will update VERBYTE only on the breaking API changes.