From 06d42ee9561d54dbaebc7c1f111dd9365c29e7d7 Mon Sep 17 00:00:00 2001 From: Ville Rantanen Date: Thu, 1 Mar 2018 15:05:29 +0200 Subject: [PATCH] reborked the token system --- README.md | 5 +- code/app.py | 87 ++++++------- code/flees-manager.py | 287 +++++++++++++++++++++++++++--------------- code/utils/crypt.py | 14 +++ 4 files changed, 241 insertions(+), 152 deletions(-) diff --git a/README.md b/README.md index 68d40df..10b5e18 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,7 @@ Operation is one of download, direct_download, zip_download, or upload - shares.json stores hashed version of password. - Additionally, it may store plain text password, if users so wish. - Internally, Flees only compares the hashes of passwords -- Tokens are encrypted versions of the hash. (login/upload/download with - direct links). i.e. decrypted URL request equals password hash -- Encryption key is the app_secret_key +- Tokens are secret strings that allow login/upload/download with + direct links. You can have many tokens for single share. - Direct download token is (password hash + filename) hashed diff --git a/code/app.py b/code/app.py index bf2a550..a9f5ed8 100644 --- a/code/app.py +++ b/code/app.py @@ -40,7 +40,6 @@ if 'notifier' in config_values: app.secret_key = config_values['app_secret_key'] app.wsgi_app = ReverseProxied(app.wsgi_app) -app.config['CRYPTO'] = Crypto(app.secret_key) @app.before_request def before_request(): @@ -80,16 +79,14 @@ def authenticate(name): session[name] = password_hash(user_password, app.secret_key) return redirect(url_for('list_view',name=name)) -@app.route('/upload//', methods=['POST']) +@app.route('/upload//', methods=['POST']) @app.route('/upload', methods=['POST']) -def upload(name = None, password = None): +def upload(name = None, token = None): if request.method == 'POST': file = request.files['file'] if name == None: name = request.form['name'] - if password != None: - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) + (ok,share) = get_share(name, token = token) if not ok: return share if not get_or_none('upload', share) == True: @@ -121,11 +118,10 @@ def upload(name = None, password = None): return "Use the 'file' variable to upload\n",400 -@app.route('/upload_join//', methods=['POST']) -def upload_join_splitted(name, password): +@app.route('/upload_join//', methods=['POST']) +def upload_join_splitted(name, token): if request.method == 'POST': - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) + (ok,share) = get_share(name, token = token) if not ok: return share if not get_or_none('upload', share) == True: @@ -173,15 +169,16 @@ def send(name): return share return render_template('send.html',name=name) -@app.route('/list//', methods=['GET']) +@app.route('/list//', methods=['GET']) @app.route('/list/', methods=['GET']) -def list_view(name, password = None): - if password != None: - session[name] = app.config['CRYPTO'].decrypt(password) - return redirect(url_for('list_view',name=name)) - (ok,share) = get_share(name) +def list_view(name, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share + if token != None and 'pass_hash' in share: + session[name] = share['pass_hash'] + return redirect(url_for('list_view',name=name)) + files = [] for file in sorted(os.listdir(share['path'])): fp = os.path.join(share['path'],file) @@ -247,12 +244,10 @@ def download_direct(name,token,filename): return send_from_directory(directory=share['path'], filename=filename) -@app.route('/download///', methods=['GET']) +@app.route('/download///', methods=['GET']) @app.route('/download//', methods=['GET']) -def download_file(name,filename,password = None): - if password != None: - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +def download_file(name, filename, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share file_path = os.path.join(share['path'], filename) @@ -267,12 +262,10 @@ def download_file(name,filename,password = None): return send_from_directory(directory=share['path'], filename=filename) -@app.route('/zip//', methods=['GET']) +@app.route('/zip//', methods=['GET']) @app.route('/zip/', methods=['GET']) -def download_zip(name,password = None): - if password != None: - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +def download_zip(name, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share folder_size = get_folder_size(share['path']) @@ -292,10 +285,9 @@ def download_zip(name,password = None): attachment_filename = name + ".zip" ) -@app.route('/script/upload//', methods=['GET']) -def script_upload(name = None, password = None): - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +@app.route('/script/upload//', methods=['GET']) +def script_upload(name = None, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share if not get_or_none('upload', share) == True: @@ -335,14 +327,13 @@ done """%( request.url_root, name, - password + token ) -@app.route('/script/download//', methods=['GET']) -def script_download(name = None, password = None): - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +@app.route('/script/download//', methods=['GET']) +def script_download(name = None, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share files = [] @@ -385,7 +376,7 @@ get_file() { """%( request.url_root, name, - password + token ) for file in files: @@ -395,10 +386,9 @@ get_file() { return script -@app.route('/script/direct//', methods=['GET']) -def script_direct(name = None, password = None): - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +@app.route('/script/direct//', methods=['GET']) +def script_direct(name = None, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share files = [] @@ -451,10 +441,9 @@ get_file() { return script -@app.route('/script/upload_split//', methods=['GET']) -def script_upload_split(name = None, password = None): - session[name] = app.config['CRYPTO'].decrypt(password) - (ok,share) = get_share(name) +@app.route('/script/upload_split//', methods=['GET']) +def script_upload_split(name = None, token = None): + (ok,share) = get_share(name, token = token) if not ok: return share if not get_or_none('upload', share) == True: @@ -509,7 +498,7 @@ done """%( request.url_root, name, - password + token ) class uploadJoiner: @@ -554,7 +543,7 @@ def file_versionize(filename): os.rename(filename,new_name) -def get_share(name, require_auth = True): +def get_share(name, require_auth = True, token = None): share = [x for x in g.shares if x['name'] == name] if len(share) < 1: return (False,redirect(url_for('authenticate',name=name))) @@ -562,6 +551,10 @@ def get_share(name, require_auth = True): if is_expired(share): return (False, 'Share has expired') authenticated = "no-pass" + if not token == None: + if has_token(token, share): + require_auth = False + authenticated = "token" if require_auth: if 'pass_hash' in share: authenticated = False @@ -584,6 +577,7 @@ def get_share(name, require_auth = True): makedirs_rights(share['path']) return (True,share) + def is_expired(share): expires = get_or_none('expire', share) if expires: @@ -607,6 +601,7 @@ def makedirs_rights(path): os.mkdir(current_path) set_rights(current_path) + def notify(msg): if 'notifier' in app.config: msg['environment'] = request.environ diff --git a/code/flees-manager.py b/code/flees-manager.py index 8425b3a..d3e39a6 100755 --- a/code/flees-manager.py +++ b/code/flees-manager.py @@ -19,20 +19,20 @@ def get_root_path(opts): def list_shares(shares,opts): table = [] - table.append(('Name', 'Path','Public','Password','PassHash','Upload','Overwrite','Direct','Expire','Recipient','Description')) + table.append(('Name', 'Path','Public','Password','Tokens','Upload','Overwrite','Direct','Expire','Recipient','Description')) for share in shares: public = get_or_none('public',share, False) - passhash = '-' + passtoken = '-' password = 'pass_hash' in share if opts.show_password: if 'pass_plain' in share: password = share['pass_plain'] else: password = "" - if 'pass_hash' in share: - passhash = share['pass_hash'] + if 'tokens' in share: + passtoken = ",".join(share['tokens']) else: - passhash = "-" + passtoken = "" upload = get_or_none('upload',share, False) overwrite = get_or_none('overwrite',share, True) direct = get_or_none('direct_links',share, False) if password else False @@ -43,7 +43,7 @@ def list_shares(shares,opts): share['path']+"/", public, password, - passhash, + passtoken, upload, overwrite, direct, @@ -102,6 +102,7 @@ def add_share(shares, config, opts): if opts.plain: share['pass_plain'] = opts.password share['pass_hash'] = password_hash(opts.password, config['app_secret_key']) + share['tokens'] = [random_token()] if opts.expire: try: date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M") @@ -138,7 +139,9 @@ def modify_share(shares, config, opts): for i,share in enumerate(shares): if share['name'] != opts.name: continue - orig_share = share.copy() + orig_share = dict(share) + if 'tokens' in share: + orig_share['tokens'] = list(share['tokens']) print(json.dumps(share, indent = 2, sort_keys = True)) found = True break @@ -167,6 +170,19 @@ def modify_share(shares, config, opts): share['pass_plain'] = opts.password share['pass_hash'] = password_hash(opts.password, config['app_secret_key']) + # Handle tokens + if opts.remove_tokens: + for token in opts.remove_tokens: + if token in share['tokens']: + share['tokens'].remove(token) + if opts.tokens: + for token in opts.tokens: + if not 'tokens' in share: + share['tokens'] = [] + if not token in share['tokens']: + share['tokens'].append(token) + + if opts.expire: if opts.expire == "": # REMOVE EXPIRATION @@ -201,7 +217,7 @@ def modify_share(shares, config, opts): if not key in orig_share: modified.append(key) continue - if orig_share[key] != share[key]: + if str(orig_share[key]) != str(share[key]): modified.append(key) continue for key in orig_share: @@ -242,109 +258,161 @@ def print_rest_api(shares, config, opts): share = shares[0] if opts.type == "list": - print("Link to enter the share:") - print("%s/list/%s"%( - config['public_url'], - share['name'] - )) + print_rest_api_list(config,share) return - if not 'pass_hash' in share: - print("REST API enabled only if pass_hash is set for share") - sys.exit(1) - crypter = Crypto(config['app_secret_key']) - crypted = crypter.encrypt(share['pass_hash']) - if opts.type == "login": - print("Link to automatically login in the share:") - print("%s/list/%s/%s"%( - config['public_url'], - share['name'], - crypted - )) - elif opts.type == "upload": - if 'upload' not in share or not share['upload']: - print("Uploading not allowed to this share") - sys.exit(0) - print("Link to upload file to the share:") - print("\n# curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%( - config['public_url'], - share['name'], - crypted - )) - print("\nLink to upload multiple files to the share:") - print("\n# curl -s %s/script/upload/%s/%s | bash /dev/stdin file_to_upload.ext [second.file.ext]"%( - config['public_url'], - share['name'], - crypted - )) - print("\nLink to upload multiple files to the share, splitting large files (default 512Mb):") - print("\n# curl -s %s/script/upload_split/%s/%s | bash /dev/stdin [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%( - config['public_url'], - share['name'], - crypted - )) + if (not 'tokens' in share) or len(share['tokens']) == 0: + print("REST API enabled only if tokens are set for the share") + sys.exit(1) + token = False + if len(share['tokens']) == 1: + token = share['tokens'][0] + else: + try: + token_int = int(opts.token) - 1 + if token_int < 0: + raise ValueError + token = share['tokens'][token_int] + except (IndexError, ValueError, TypeError) as e: + if opts.token in share['tokens']: + token = opts.token + if not token: + # more tokens! + + if opts.token: + print("No such token for this share") + print("Tokens:") + for i,token in enumerate(share['tokens']): + print("%d. %s"%( i+1, token )) + print("Run again with --token [nr]") + if not opts.token: + sys.exit(0) + else: + sys.exit(1) + if opts.type == "login": + print_rest_api_login(config,share,token) + elif opts.type == "upload": + print_rest_api_upload(config,share,token) elif opts.type == "download": - print("Links to download files:") - share_path = os.path.join( - config['__root_path__'], - config['data_folder'], - share['path'] - ) - if not os.path.exists(share_path): - print("no files") - sys.exit(0) - for filename in sorted(os.listdir(share_path)): - if os.path.isdir(os.path.join(share_path,filename)): - continue - if filename.startswith("."): - continue - print("%s/download/%s/%s/%s"%( - config['public_url'], - share['name'], - crypted, - filename - )) - print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%( - config['public_url'], - share['name'], - crypted - )) + print_rest_api_download(config, share, token) elif opts.type == "direct": - if 'direct_links' not in share or not share['direct_links']: - print("Direct downloading not allowed in this share") - sys.exit(0) - print("Links to direct download files:") - share_path = os.path.join( - config['__root_path__'], - config['data_folder'], - share['path'] - ) - if not os.path.exists(share_path): - print("no files") - sys.exit(0) - for filename in sorted(os.listdir(share_path)): - if os.path.isdir(os.path.join(share_path,filename)): - continue - if filename.startswith("."): - continue - print("%s/direct/%s/%s/%s"%( - config['public_url'], - share['name'], - get_direct_token(share,filename), - filename - )) - print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%( - config['public_url'], - share['name'], - crypted - )) + print_rest_api_direct(config, share, token) elif opts.type == "zip": - print("ZIP download:") - print("%s/zip/%s/%s"%( + print_rest_api_zip(config, share, token) + + +def print_rest_api_list(config, share): + print("Link to enter the share:") + print("%s/list/%s"%( + config['public_url'], + share['name'] + )) + + +def print_rest_api_login(config, share, token): + print("Link to automatically login in the share:") + print("%s/list/%s/%s"%( + config['public_url'], + share['name'], + token + )) + + +def print_rest_api_upload(config, share, token): + if 'upload' not in share or not share['upload']: + print("Uploading not allowed to this share") + sys.exit(0) + print("Link to upload file to the share:") + print("\n# curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%( + config['public_url'], + share['name'], + token + )) + print("\nLink to upload multiple files to the share:") + print("\n# curl -s %s/script/upload/%s/%s | bash /dev/stdin file_to_upload.ext [second.file.ext]"%( + config['public_url'], + share['name'], + token + )) + print("\nLink to upload multiple files to the share, splitting large files (default 512Mb):") + print("\n# curl -s %s/script/upload_split/%s/%s | bash /dev/stdin [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%( + config['public_url'], + share['name'], + token + )) + + +def print_rest_api_download(config, share, token): + print("Links to download files:") + share_path = os.path.join( + config['__root_path__'], + config['data_folder'], + share['path'] + ) + if not os.path.exists(share_path): + print("no files") + sys.exit(0) + for filename in sorted(os.listdir(share_path)): + if os.path.isdir(os.path.join(share_path,filename)): + continue + if filename.startswith("."): + continue + print("%s/download/%s/%s/%s"%( config['public_url'], share['name'], - crypted + token, + filename )) + print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%( + config['public_url'], + share['name'], + token + )) + + + +def print_rest_api_direct(config, share, token): + if 'direct_links' not in share or not share['direct_links']: + print("Direct downloading not allowed in this share") + sys.exit(0) + print("Links to direct download files:") + share_path = os.path.join( + config['__root_path__'], + config['data_folder'], + share['path'] + ) + if not os.path.exists(share_path): + print("no files") + sys.exit(0) + for filename in sorted(os.listdir(share_path)): + if os.path.isdir(os.path.join(share_path,filename)): + continue + if filename.startswith("."): + continue + print("%s/direct/%s/%s/%s"%( + config['public_url'], + share['name'], + get_direct_token(share,filename), + filename + )) + print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%( + config['public_url'], + share['name'], + token + )) + + +def print_rest_api_zip(config, share, token): + print("ZIP download:") + print("%s/zip/%s/%s"%( + config['public_url'], + share['name'], + token + )) + + +def print_token(): + print(random_token()) def parse_options(): @@ -431,6 +499,12 @@ def parse_options(): parser_modify.add_argument('-r','--recipient', action="store", dest="recipient", default = None, help= "Recipient for notifications (if enabled)" ) + parser_modify.add_argument('-t','--add-token', action="append", dest="tokens", default = [], + help= "Token for REST api, may be issued multiple times" + ) + parser_modify.add_argument('--remove-token', action="append", dest="remove_tokens", default = [], + help= "Remove REST tokens, may be issued multiple times" + ) parser_modify.add_argument('-w','--write', action="store_true", dest="write", default = False, help = "Write changes to the shares.json file" ) @@ -440,6 +514,11 @@ def parse_options(): parser_rest.add_argument(dest="type", help = "Type of command", choices = ['list','login','upload','download','direct','zip'] ) + parser_rest.add_argument('-t','--token', action="store", dest="token", default = None, + help= "If share has multiple tokens, select one to print REST API for." + ) + ## TOKEN + parser_token = subparsers.add_parser('token', help = "Generate a random token") return parser.parse_args() @@ -477,6 +556,8 @@ if __name__ == "__main__": modify_share(shares,config,opts) elif opts.subparser_name == 'rest': print_rest_api(shares,config,opts) + elif opts.subparser_name == 'token': + print_token() diff --git a/code/utils/crypt.py b/code/utils/crypt.py index e4f7d42..ecd8f25 100644 --- a/code/utils/crypt.py +++ b/code/utils/crypt.py @@ -1,3 +1,5 @@ +import random +import string import base64 from Crypto.Cipher import AES import hashlib @@ -44,6 +46,12 @@ def get_direct_token(share, filename): ) +def has_token(token, share): + if not 'tokens' in share: + return False + return token in share['tokens'] + + def password_hash(string, salt=""): if type(string) == str: string = string.encode("utf-8") @@ -54,6 +62,12 @@ def password_hash(string, salt=""): ).hexdigest() +def random_token(): + chars = [random.choice(string.ascii_letters + string.digits) for n in range(30)] + token = "".join(chars) + return token + + def remove_pad(string): """ Remove spaces from right """ return string.rstrip(" ")