import json import os import requests import tempfile import Cryptodome.Cipher.AES import Cryptodome.Hash.HMAC import Cryptodome.Hash.SHA256 import requests_toolbelt from sendclient.common import unpadded_urlsafe_b64encode, unpadded_urlsafe_b64decode, secretKeys, SPOOL_SIZE, CHUNK_SIZE, checkServerVersion, progbar, fileSize import sendclient.password def encrypt_file(file, keys=secretKeys()): '''Encrypt file data with the same method as the Send browser/js client''' key = keys.encryptKey iv = keys.encryptIV encData = tempfile.SpooledTemporaryFile(max_size=SPOOL_SIZE, mode='w+b') cipher = Cryptodome.Cipher.AES.new(key, Cryptodome.Cipher.AES.MODE_GCM, iv) pbar = progbar(fileSize(file)) for chunk in iter(lambda: file.read(CHUNK_SIZE), b''): encData.write(cipher.encrypt(chunk)) pbar.update(len(chunk)) pbar.close() encData.write(cipher.digest()) file.close() encData.seek(0) return encData def encrypt_metadata(keys, fileName, fileType='application/octet-stream'): '''Encrypt file metadata with the same method as the Send browser/js client''' metadata = json.dumps({'iv' : unpadded_urlsafe_b64encode(keys.encryptIV), 'name' : fileName, 'type' : fileType}, sort_keys=True) cipher = Cryptodome.Cipher.AES.new(keys.metaKey, Cryptodome.Cipher.AES.MODE_GCM, keys.metaIV) encMeta, gcmTag = cipher.encrypt_and_digest(metadata.encode()) # WebcryptoAPI expects the gcm tag at the end of the ciphertext, return them concatenated return encMeta + gcmTag def api_upload(service, encData, encMeta, keys): ''' Uploads data to Send. Caution! Data is uploaded as given, this function will not encrypt it for you ''' service += 'api/upload' files = requests_toolbelt.MultipartEncoder(fields={'file': ('blob', encData, 'application/octet-stream') }) pbar = progbar(files.len) monitor = requests_toolbelt.MultipartEncoderMonitor(files, lambda files: pbar.update(monitor.bytes_read - pbar.n)) headers = { 'X-File-Metadata' : unpadded_urlsafe_b64encode(encMeta), 'Authorization' : 'send-v1 ' + unpadded_urlsafe_b64encode(keys.authKey), 'Content-type' : monitor.content_type } r = requests.post(service, data=monitor, headers=headers, stream=True) r.raise_for_status() pbar.close() body_json = r.json() secretUrl = body_json['url'] + '#' + unpadded_urlsafe_b64encode(keys.secretKey) fileId = body_json['id'] fileNonce = unpadded_urlsafe_b64decode(r.headers['WWW-Authenticate'].replace('send-v1 ', '')) try: owner_token = body_json['owner'] except: owner_token = body_json['delete'] return secretUrl, fileId, fileNonce, owner_token def sign_nonce(key, nonce): ''' sign the server nonce from the WWW-Authenticate header with an authKey''' # HMAC.new(key, msg='', digestmod=None) return Cryptodome.Hash.HMAC.new(key, nonce, digestmod=Cryptodome.Hash.SHA256).digest() def send_file(service, file, fileName=None, password=None, ignoreVersion=False): if checkServerVersion(service, ignoreVersion=ignoreVersion) == False: print('\033[1;41m!!! Potentially incompatible server version !!!\033[0m') ''' Encrypt & Upload a file to send and return the download URL''' fileName = fileName if fileName != None else os.path.basename(file.name) print('Encrypting data from "' + fileName + '"') keys = secretKeys() encData = encrypt_file(file, keys) encMeta = encrypt_metadata(keys, fileName) print('Uploading "' + fileName + '"') secretUrl, fileId, fileNonce, owner_token = api_upload(service, encData, encMeta, keys) if password != None: print('Setting password') sendclient.password.set_password(secretUrl, owner_token, password) return secretUrl, fileId, owner_token