# Secrets view import time import random from sqlalchemy import or_, func from tabulate import tabulate from passwordgenerator import pwgenerator from ..models.base import get_session from ..models.Secret import SecretModel from ..modules.misc import confirm, clear_screen from ..modules.carry import global_scope from ..modules import autocomplete from .categories import get_name as get_category_name, pick, all as all_categories from . import clipboard, menu def all(): """ Return a list of all secrets """ return get_session().query(SecretModel).order_by(SecretModel.id).all() def to_table(rows=[]): """ Transform rows in a table """ # Retrieve id and name all_secrets = [[secret.id, get_category_name(secret.category_id), secret.name, secret.url, secret.login] for secret in rows] if len(all_secrets) > 0: return tabulate(all_secrets, headers=['Item', 'Category', 'Name', 'URL', 'Login']) else: return 'Empty!' def count(): """ Return a count of all secrets """ return get_session().query(SecretModel).count() def get_by_id(id_): """ Get a secret by ID """ return get_session().query(SecretModel).get(int(id_)) def get_names(limit=2000): """ Return secret's names for auto-completion """ results = get_session().query(SecretModel.name).\ filter(SecretModel.name != '').\ limit(limit).\ all() if results: return [result.name for result in results] return [] def get_top_logins(limit=10): """ Return most popular logins for auto-completion """ count_ = func.count('*') results = get_session().query(SecretModel.login, count_).\ filter(SecretModel.login != '').\ group_by(SecretModel.login).\ order_by(count_.desc()).\ limit(limit).\ all() if results: return [result.login for result in results] return [] def add(name, url='', login='', password='', notes='', category_id=None): """ Create a new secret """ secret = SecretModel(name=name, url=url, login=login, password=password, notes=notes, category_id=category_id) get_session().add(secret) get_session().commit() return True def add_input(): """ Ask user for a secret details and create it """ # Clear screen clear_screen() # Ask user input category_id = None if len(all_categories()) > 0: category_id = pick( message='* Choose a category number (or leave empty for none): ', optional=True) if category_id is False: return False name = menu.get_input(message='* Name: ') if name is False: return False url = menu.get_input(message='* URL: ') if url is False: return False # Get list for auto-completion autocomplete.set_parameters(list_=get_top_logins(), case_sensitive=True) login = autocomplete.get_input_autocomplete( message='* Login (use [tab] for autocompletion): ') if login is False: return False suggestion = pwgenerator.generate() print('* Password suggestion: %s' % (suggestion)) password = menu.get_input( message='* Password: ', secure=True) if password is False: return False notes = notes_input() if notes is False: return False # Save item add(name=name, url=url, login=login, password=password, notes=notes, category_id=category_id or None) print() print('The new item has been saved to your vault.') print() time.sleep(2) return True def notes_input(): """ Ask user to input notes """ print('* Notes: (press [ENTER] twice to complete)') notes = [] for i in range(15): # Max 15 lines input_ = menu.get_input(message="> ") if input_ is False: return False elif input_ == "": break else: notes.append(input_) return "\n".join(notes) def delete(id_): """ Delete a secret """ secret = get_session().query(SecretModel).filter( SecretModel.id == int(id_)).first() if secret: get_session().delete(secret) get_session().commit() return True return False def delete_confirm(id_): """ Delete a secret (ID is an input, just asking for confirmation) """ if confirm('Confirm deletion?', False): result = delete(id_) if result is True: print() print('The secret has been deleted.') time.sleep(2) return result return False def search(query): """ Search by keyword """ query = '%' + str(query) + '%' return get_session().query(SecretModel) \ .filter(or_(SecretModel.name.like(query), SecretModel.url.like(query), SecretModel.login.like(query))) \ .order_by(SecretModel.id).all() def search_dispatch(query): """ Run a user search. If the query is an integer we will first search by id, otherwise, it will be a keyword based search """ if type(query) is int or query.isdigit(): # Search an ID matching the input row = get_by_id(int(query)) if row: return [row] # Otherwise return search result return search(query) def search_input(): """ Ask user to input a search query """ # Ask user input print() autocomplete.set_parameters(list_=get_names(), case_sensitive=False) query = autocomplete.get_input_autocomplete( message='Enter search: ') if not query: print() print('Empty search!') return False # To prevent fat-finger errors, the search menu will also respond to common commands if query in ['s', 'a', 'l', 'q']: # Common commands return query elif query == 'b': # Return to previous menu return False # Get results results = search_dispatch(query) if len(results) == 1: # Exactly one result return item_view(results[0]) elif len(results) > 1: # More than one result return search_results(results) else: try: print('No results!') time.sleep(2) except KeyboardInterrupt: pass except Exception: # Other Exception pass return False def search_results(rows): """ Display search results """ print() print(to_table(rows)) print() # Ask user input input_ = menu.get_input( message='Select a result # or type any key to go back to the main menu: ') if input_: try: result = [row for row in rows if row.id == int(input_)] if result: return item_view(result[0]) except ValueError: # Non integer pass return False def item_view(item): """ Show a secret """ # Clear screen clear_screen() print(to_table([item])) print() # Show eventual notes if item.notes: print('Notes:') print(item.notes) print() # Show item menu return item_menu(item) def item_menu(item): """ Item menu """ while True: command = menu.get_input( message='Choose a command [%s%s%s%s%s%s / (e)dit / (d)elete / (s)earch / (b)ack to Vault]: ' % ( 'copy ' if item.login or item.password or item.url else '', '(l)ogin, ' if item.login else '', '(p)assword' if item.password else '', ' or ' if ( item.login or item.password) and item.url else '', '(u)rl to clipboard' if item.url else '', ' / sh(o)w password' if item.password else '', ), lowercase=True, non_locking_values=['l', 'q'] ) if command is False: print() # Action based on command if command == 'l': # Copy login to the clipboard clipboard.copy(item.login, 'login') clipboard.wait() elif command == 'p': # Copy a secret to the clipboard clipboard.copy(item.password) clipboard.wait() elif command == 'u': # Copy URL to the clipboard clipboard.copy(item.url, 'URL') clipboard.wait() elif command == 'o': # Show a secret return show_secret(item) elif command == 'e': # Edit an item item_menu_edit(item) elif command == 'd': # Delete an item delete_confirm(item.id) return elif command in ['s', 'b', 'q']: # Common commands return command def item_menu_edit(item): """ Edit an item """ command = menu.get_input( message='Choose what you would like to edit [(c)ategory / (n)ame / (u)rl / (l)ogin / (p)assword / n(o)tes / (b)ack to Vault]: ', lowercase=True, non_locking_values=['l', 'q'] ) # Action based on command if command == 'c': # Edit category edit_input('category', item) return elif command == 'n': # Edit name edit_input('name', item) return elif command == 'u': # Edit URL edit_input('url', item) return elif command == 'l': # Edit login edit_input('login', item) return elif command == 'p': # Edit password edit_input('password', item) return elif command == 'o': # Edit notes edit_input('notes', item) return elif command == 'b': # Back to vault menu return return def edit_input(element_name, item): """ Edit an item """ if element_name == 'category': print('* Current nategory: %s' % (get_category_name(item.category_id) or 'Empty!')) category_id = pick(message='* New category: ', optional=True) if category_id is not False: item.category_id = category_id else: time.sleep(2) print('\nCancelled!') return False elif element_name == 'name': print('* Current name: %s' % (item.name or 'Empty!')) name = menu.get_input(message='* New name: ') if name is not False: item.name = name else: print('\nCancelled!') time.sleep(2) return False elif element_name == 'url': print('* Current URL: %s' % (item.url or 'Empty!')) url = menu.get_input(message='* New URL: ') if url is not False: item.url = url else: print('\nCancelled!') time.sleep(2) return False elif element_name == 'login': print('* Current login: %s' % (item.login or 'Empty!')) login = menu.get_input(message='* New login: ') if login is not False: item.login = login else: print('\nCancelled!') time.sleep(2) return False elif element_name == 'password': print('* Password suggestion: %s' % (pwgenerator.generate())) password = menu.get_input(message='* New password: ', secure=True) if password is not False: item.password = password else: print('\nCancelled!') time.sleep(2) return False elif element_name == 'notes': print('* Current notes: %s' % (item.notes or 'Empty!')) notes = notes_input() if notes is not False: item.notes = notes else: print('\nCancelled!') time.sleep(2) return False else: raise ValueError('Element `%s` not not exists.' % (element_name)) # Process update get_session().add(item) get_session().commit() print('The %s has been updated.' % (element_name)) time.sleep(2) return True def show_secret(item): """ Show a secret for X seconds and erase it from the screen """ try: print("* The password will be hidden after %s seconds." % (global_scope['conf'].hideSecretTTL)) print('* The password is: %s' % (item.password), end="\r") time.sleep(int(global_scope['conf'].hideSecretTTL)) except KeyboardInterrupt: # Will catch `^-c` and immediately hide the password pass print('* The password is: ' + '*' * ( len(item.password) + random.randint(1, 8))) return item_view(item)