def on_correct_password(typo_db, password): # log the entry of the original pwd logger.info("-^-") try: if not typo_db.is_typtop_init(): logger.error("Typtop DB wasn't initiated yet!") typo_db.init_typtop(password) # the initialization is now part of the installation process check_system_status(typo_db) # correct password but db fails to see it is_match = typo_db.check(password) if not is_match: logger.debug("Changing system status to {}.".format( SYSTEM_STATUS_PW_CHANGED )) typo_db.set_status(SYSTEM_STATUS_PW_CHANGED) logger.info("Changing the system status because: match={}".format(is_match)) except (ValueError, KeyError) as e: # most probably - an error of decryption as a result of pw change typo_db.set_status(SYSTEM_STATUS_PW_CHANGED) logger.exception("Key error raised. Probably a failure in decryption. Re-initializing...") typo_db.reinit_typtop(password) except Exception as e: logger.exception( "Unexpected error while on_correct_password:\n{}\n" .format(e) ) # In order to avoid locking out - always return true for correct password return True
def on_correct_password(typo_db, password): # log the entry of the original pwd logger.info("-^-") try: if not typo_db.is_typtop_init(): logger.error("Typtop DB wasn't initiated yet!") typo_db.init_typtop(password) # the initialization is now part of the installation process check_system_status(typo_db) # correct password but db fails to see it is_match = typo_db.check(password) if not is_match: logger.debug("Changing system status to {}.".format( SYSTEM_STATUS_PW_CHANGED)) typo_db.set_status(SYSTEM_STATUS_PW_CHANGED) logger.info("Changing the system status because: match={}".format( is_match)) except (ValueError, KeyError) as e: # most probably - an error of decryption as a result of pw change typo_db.set_status(SYSTEM_STATUS_PW_CHANGED) logger.exception( "Key error raised. Probably a failure in decryption. Re-initializing..." ) typo_db.reinit_typtop(password) except Exception as e: logger.exception( "Unexpected error while on_correct_password:\n{}\n".format(e)) # In order to avoid locking out - always return true for correct password return True
def _decrypt_n_filter_waitlist(self): """Decrypts the waitlist and filters out the ones failed validity check. After that it combines the typos and returns a list of typos sorted by their frequency. return: [(typo_i, f_i)] """ filtered_typos = defaultdict(int) sk = deserialize_sk(self._sk) assert self._pwent >= 0, \ "pw={} is not initialized: {}".format(self._pw, self._pwent) ignore = set() install_id = self.get_installation_id() for typo_ctx in self.get_from_auxtdb(WAIT_LIST): # , yaml.load): typo_txt = pkdecrypt(sk, typo_ctx) typo, ts = yaml.safe_load(typo_txt) # starts with installation id, then must be garbage if typo.startswith(install_id): continue self.insert_log(typo, in_cache=False, ts=ts) if typo in ignore: continue if self.validate(self._pw, typo): filtered_typos[typo] += 1 else: logger.debug("Ignoring: {}".format(typo)) ignore.add(typo) logger.info("Waitlist decrypted successfully") return sorted( filtered_typos.items(), key=lambda a: a[1], reverse=True )
def insert_log(self, typo, in_cache, ts=None): # type: (str, bool, int) -> None """Updates the log with information about typo. Remember, if sk_dict is not provided it will insert @typo as typo_id and 0 as relative_entropy. Note the default values used in other_info, which is basically what is expected for the original password. """ assert self._pw and self._hmac_salt rel_ent = entropy(typo) - self._pwent if rel_ent == -self._pwent: logger.debug( 'typo (={!r}) is an empty string, should not happen!!'.format( typo)) # cap rel_ent to the +10, -10 rel_ent = max(-10, min(rel_ent, 10)) edit_dist = min(5, distance(str(self._pw), str(typo))) log_info = { 'tid': compute_id(self._hmac_salt, typo), 'edit_dist': edit_dist, 'rel_entropy': rel_ent, 'ts': get_time if ts is None else ts, 'localtime': time.asctime(), # without zone, but gives local time in string. 'istop5fixable': is_in_top5_fixes(self._pw, typo), 'in_cache': in_cache } try: self._db[logT].append(log_info) except KeyError: self._db[logT] = [log_info]
def insert_log(self, typo, in_cache, ts=None): # type: (str, bool, int) -> None """Updates the log with information about typo. Remember, if sk_dict is not provided it will insert @typo as typo_id and 0 as relative_entropy. Note the default values used in other_info, which is basically what is expected for the original password. """ assert self._pw and self._hmac_salt rel_ent = entropy(typo) - self._pwent if rel_ent == -self._pwent: logger.debug('typo (={!r}) is an empty string, should not happen!!' .format(typo)) # cap rel_ent to the +10, -10 rel_ent = max(-10, min(rel_ent, 10)) edit_dist = min(5, distance(str(self._pw), str(typo))) log_info = { 'tid': compute_id(self._hmac_salt, typo), 'edit_dist': edit_dist, 'rel_entropy': rel_ent, 'ts': get_time if ts is None else ts, 'localtime': time.asctime(), # without zone, but gives local time in string. 'istop5fixable': is_in_top5_fixes(self._pw, typo), 'in_cache': in_cache } try: self._db[logT].append(log_info) except KeyError: self._db[logT] = [log_info]
def _decrypt_n_filter_waitlist(self): """Decrypts the waitlist and filters out the ones failed validity check. After that it combines the typos and returns a list of typos sorted by their frequency. return: [(typo_i, f_i)] """ filtered_typos = defaultdict(int) sk = deserialize_sk(self._sk) assert self._pwent >= 0, \ "pw={} is not initialized: {}".format(self._pw, self._pwent) ignore = set() install_id = self.get_installation_id() for typo_ctx in self.get_from_auxtdb(WAIT_LIST): # , yaml.load): typo_txt = pkdecrypt(sk, typo_ctx) typo, ts = yaml.safe_load(typo_txt) # starts with installation id, then must be garbage if typo.startswith(install_id): continue self.insert_log(typo, in_cache=False, ts=ts) if typo in ignore: continue if self.validate(self._pw, typo): filtered_typos[typo] += 1 else: logger.debug("Ignoring: {}".format(typo)) ignore.add(typo) logger.info("Waitlist decrypted successfully") return sorted(filtered_typos.items(), key=lambda a: a[1], reverse=True)
def entropy(typo): ent = 0 if typo not in _entropy_cache: if not typo or len(typo) == 0: _entropy_cache[typo] = 0 else: try: n_guesses = zxcvbn(typo)['guesses'] _entropy_cache[typo] = math.log(n_guesses) except IndexError as e: logger.exception(e) logger.debug(typo) return _entropy_cache[typo]
def entropy(typo): ent = 0 if typo not in _entropy_cache: if not typo or len(typo) == 0: _entropy_cache[typo] = 0 else: try: n_guesses = zxcvbn(typo)['guesses'] _entropy_cache[typo] = math.log(n_guesses) except IndexError as e: logger.exception(e) logger.debug(typo) return _entropy_cache[typo]
def _update_typo_cache_by_waitlist(self, typo_cache, freq_counts): """ Updates the hash cache according to waitlist and clears the waitlist @typo_cache: a list of typos in the cache, and @freq_counts are the corresponding frequencies. returns: Updated typo_cache and their frequencies. Also applies the permutations. """ logger.info("Updating TypoCache by Waitlist") good_typo_list = self._decrypt_n_filter_waitlist() mini, minf = min(enumerate(freq_counts), key=itemgetter(1)) for typo, f in good_typo_list: if UserTypoDB.cache_insert_policy(minf, f): logger.debug("Inserting: {} @ {}".format(typo, mini)) typo_cache[mini+1] = pwencrypt(typo, self._sk) freq_counts[mini] = max(minf + 1, f) # TODO: Check mini, minf = min(enumerate(freq_counts), key=itemgetter(1)) else: logger.debug("I miss you: {} ({} <-> {})".format(typo, minf, f)) logger.debug("Freq counts: {}".format(freq_counts)) # write the new typo_cache and freq_list # TODO: Apply permutation header_ctx = pkencrypt(self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) logger.debug("Real pw={!r}".format(self._pw)) self.set_in_auxtdb(HEADER_CTX, header_ctx) self.set_in_auxtdb(TYPO_CACHE, typo_cache) self.clear_waitlist()
def reinit_typtop(self, newPw): """ Re-initiate the DB after a pw change. Most peripherial system settings don't change, including installID generates a new hmac salt, and encrypts the new pw, pw_ent, and the hmac salt """ if not self.is_typtop_init(): self.init_typtop(newPw) # Mostly a simple copy-paste of steps 1 to 2.5 logger.info("Re-intializing after a pw change") # 1. derive public_key from the original password # 2. encrypt the global salt with the enc pk self._hmac_salt = os.urandom(16) # global salt pk, sk = generate_key_pair() # ECC key pair self._pk, self._sk = pk, serialize_sk(sk) self._pw = newPw perm_index = self._fill_cache_w_garbage() if WARM_UP_CACHE: freq_counts = range(CACHE_SIZE, 0, -1) for i, f in enumerate(range(CACHE_SIZE)): freq_counts[perm_index[i]] = freq_counts[i] else: freq_counts = [0 for _ in range(CACHE_SIZE)] header_ctx = pkencrypt( self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) self.set_in_auxtdb(HEADER_CTX, header_ctx) self.set_in_auxtdb(ENC_PK, serialize_pk(self._pk)) # 3 sending logs and deleting tables: logger.debug("Sending logs, deleting tables") # 4. Filling the Typocache with garbage self._fill_waitlist_w_garbage() self.set_status(SYSTEM_STATUS_ALL_GOOD) logger.info("RE-Initialization Complete")
def get_last_unsent_logs_iter(self, force=False): """ Check what was the last time the log has been sent, And returns whether the log should be sent """ logger.debug("Getting last unsent logs") if not self.is_typtop_init(): logger.debug("Could not send. Typtop not initiated") return False, iter([]) if not self.is_allowed_upload(): logger.info("Not sending logs because send status set to False") return False, iter([]) last_sending = self.get_from_auxtdb(LOG_LAST_SENTTIME) # , float) update_gap = self.get_from_auxtdb(LOG_SENT_PERIOD) # , float) time_now = time.time() passed_enough_time = ((time_now - last_sending) >= update_gap) if not force and not passed_enough_time: logger.debug("Not enough time has passed ({}) to send new logs." .format(str(last_sending))) return False, iter([]) log_t = self._db[logT] try: new_logs = iter(log_t) # .find(log_t.table.columns.ts >= last_sending) logger.info("Prepared new logs to be sent, from {} to {}".format( str(last_sending), str(time_now)) ) return True, new_logs except AttributeError: return False, iter([])
def get_last_unsent_logs_iter(self, force=False): """ Check what was the last time the log has been sent, And returns whether the log should be sent """ logger.debug("Getting last unsent logs") if not self.is_typtop_init(): logger.debug("Could not send. Typtop not initiated") return False, iter([]) if not self.is_allowed_upload(): logger.info("Not sending logs because send status set to False") return False, iter([]) last_sending = self.get_from_auxtdb(LOG_LAST_SENTTIME) # , float) update_gap = self.get_from_auxtdb(LOG_SENT_PERIOD) # , float) time_now = time.time() passed_enough_time = ((time_now - last_sending) >= update_gap) if not force and not passed_enough_time: logger.debug( "Not enough time has passed ({}) to send new logs.".format( str(last_sending))) return False, iter([]) log_t = self._db[logT] try: new_logs = iter( log_t) # .find(log_t.table.columns.ts >= last_sending) logger.info("Prepared new logs to be sent, from {} to {}".format( str(last_sending), str(time_now))) return True, new_logs except AttributeError: return False, iter([])
def _fill_cache_w_garbage(self): logger.debug("Filling Typocache with garbage") perm_index = range(CACHE_SIZE) random.shuffle(perm_index) pw = self._pw popular_typos = [os.urandom(16) for _ in range(CACHE_SIZE)] self._pwent = entropy(self._pw) # if WARM_UP_CACHE: # No need to check, assumes always WARM_UP i = 0 for tpw in warm_up_with(pw): if WARM_UP_CACHE and i < CACHE_SIZE and pw != tpw and tpw not in popular_typos: self.insert_log(typo=tpw, in_cache=True, ts=-1) popular_typos[perm_index[i]] = tpw i += 1 elif pw != tpw: self.insert_log(typo=tpw, in_cache=False, ts=-1) popular_typos = [pw] + popular_typos garbage_list = [pwencrypt(tpw, self._sk) for tpw in popular_typos] self.set_in_auxtdb(TYPO_CACHE, garbage_list) return perm_index
def reinit_typtop(self, newPw): """ Re-initiate the DB after a pw change. Most peripherial system settings don't change, including installID generates a new hmac salt, and encrypts the new pw, pw_ent, and the hmac salt """ if not self.is_typtop_init(): self.init_typtop(newPw) # Mostly a simple copy-paste of steps 1 to 2.5 logger.info("Re-intializing after a pw change") # 1. derive public_key from the original password # 2. encrypt the global salt with the enc pk self._hmac_salt = os.urandom(16) # global salt pk, sk = generate_key_pair() # ECC key pair self._pk, self._sk = pk, serialize_sk(sk) self._pw = newPw perm_index = self._fill_cache_w_garbage() if WARM_UP_CACHE: freq_counts = range(CACHE_SIZE, 0, -1) for i, f in enumerate(range(CACHE_SIZE)): freq_counts[perm_index[i]] = freq_counts[i] else: freq_counts = [0 for _ in range(CACHE_SIZE)] header_ctx = pkencrypt(self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) self.set_in_auxtdb(HEADER_CTX, header_ctx) self.set_in_auxtdb(ENC_PK, serialize_pk(self._pk)) # 3 sending logs and deleting tables: logger.debug("Sending logs, deleting tables") # 4. Filling the Typocache with garbage self._fill_waitlist_w_garbage() self.set_status(SYSTEM_STATUS_ALL_GOOD) logger.info("RE-Initialization Complete")
def update_last_log_sent_time(self, sent_time=0, delete_old_logs=True): logger.debug("updating log sent time") if not sent_time: sent_time = get_time() logger.debug("generating new timestamp={} ".format(sent_time)) self._db[auxT][LOG_LAST_SENTTIME] = float(sent_time) if delete_old_logs: logger.debug("deleting old logs") del self._db[logT][:]
def update_last_log_sent_time(self, sent_time=0, delete_old_logs=True): logger.debug("updating log sent time") if not sent_time: sent_time = get_time() logger.debug("generating new timestamp={} ".format(sent_time)) self._db[auxT][LOG_LAST_SENTTIME] = float(sent_time) if delete_old_logs: logger.debug("deleting old logs") del self._db[logT][:]
def _fill_cache_w_garbage(self): logger.debug("Filling Typocache with garbage") perm_index = range(CACHE_SIZE) random.shuffle(perm_index) pw = self._pw popular_typos = [os.urandom(16) for _ in range(CACHE_SIZE)] self._pwent = entropy(self._pw) # if WARM_UP_CACHE: # No need to check, assumes always WARM_UP i = 0 for tpw in warm_up_with(pw): if WARM_UP_CACHE and i < CACHE_SIZE and pw != tpw and tpw not in popular_typos: self.insert_log(typo=tpw, in_cache=True, ts=-1) popular_typos[perm_index[i]] = tpw i += 1 elif pw != tpw: self.insert_log(typo=tpw, in_cache=False, ts=-1) popular_typos = [pw] + popular_typos garbage_list = [ pwencrypt(tpw, self._sk) for tpw in popular_typos ] self.set_in_auxtdb(TYPO_CACHE, garbage_list) return perm_index
def _add_typo_to_waitlist(self, typo): """Adds the typo to the waitlist. @typo (string) : typo of the user's passwrod """ logger.debug("Adding a new typo to waitlist") logger.debug("Adding: {}".format(typo)) waitlist = self.get_from_auxtdb(WAIT_LIST) # , yaml.load) indexj = int(self.get_from_auxtdb(INDEX_J)) # , int)) ts = get_time() assert indexj < len(waitlist), \ "Index_j={}, wait-list={}".format(indexj, waitlist) waitlist[indexj] = pkencrypt(self.get_pk(), json.dumps([typo, ts])) indexj = (indexj + 1) % WAITLIST_SIZE self.set_in_auxtdb(WAIT_LIST, waitlist) self.set_in_auxtdb(INDEX_J, indexj) logger.debug("Typo encrypted.")
def _add_typo_to_waitlist(self, typo): """Adds the typo to the waitlist. @typo (string) : typo of the user's passwrod """ logger.debug("Adding a new typo to waitlist") logger.debug("Adding: {}".format(typo)) waitlist = self.get_from_auxtdb(WAIT_LIST) # , yaml.load) indexj = int(self.get_from_auxtdb(INDEX_J)) # , int)) ts = get_time() assert indexj < len(waitlist), \ "Index_j={}, wait-list={}".format(indexj, waitlist) waitlist[indexj] = pkencrypt(self.get_pk(), json.dumps([typo, ts])) indexj = (indexj + 1) % WAITLIST_SIZE self.set_in_auxtdb(WAIT_LIST, waitlist) self.set_in_auxtdb(INDEX_J, indexj) logger.debug("Typo encrypted.")
def _update_typo_cache_by_waitlist(self, typo_cache, freq_counts): """ Updates the hash cache according to waitlist and clears the waitlist @typo_cache: a list of typos in the cache, and @freq_counts are the corresponding frequencies. returns: Updated typo_cache and their frequencies. Also applies the permutations. """ logger.info("Updating TypoCache by Waitlist") good_typo_list = self._decrypt_n_filter_waitlist() mini, minf = min(enumerate(freq_counts), key=itemgetter(1)) for typo, f in good_typo_list: if UserTypoDB.cache_insert_policy(minf, f): logger.debug("Inserting: {} @ {}".format(typo, mini)) typo_cache[mini + 1] = pwencrypt(typo, self._sk) freq_counts[mini] = max(minf + 1, f) # TODO: Check mini, minf = min(enumerate(freq_counts), key=itemgetter(1)) else: logger.debug("I miss you: {} ({} <-> {})".format( typo, minf, f)) logger.debug("Freq counts: {}".format(freq_counts)) # write the new typo_cache and freq_list # TODO: Apply permutation header_ctx = pkencrypt( self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) logger.debug("Real pw={!r}".format(self._pw)) self.set_in_auxtdb(HEADER_CTX, header_ctx) self.set_in_auxtdb(TYPO_CACHE, typo_cache) self.clear_waitlist()
def init_typtop(self, pw, allow_typo_login=True): """Create the 'typtop' database in user's home-directory. Changes Also, it initializes the required tables as well as the reuired variables, such as, the typo-cache size, the global salt etc. """ logger.info("Initiating typtop db with {}".format( dict(allow_typo_login=allow_typo_login))) change_db_ownership(self._db_path) # db[auxT].delete() # make sure there's no old unrelevent data # doesn't delete log because it will also be used # whenever a password is changed # *************** Initializing Aux Data ************************* # *************** add org password, its' pks && global salt: ******** # 1. derive public_key from the original password # 2. encrypt the global salt with the enc pk install_id = get_machine_id() install_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) last_sent_time = get_time() self._hmac_salt = os.urandom(SALT_LENGTH) # global salt self._pk, self._sk = generate_key_pair() # ECC key pair self._sk = serialize_sk(self._sk) self._pw = pw self._aux_tab.update({ INSTALLATION_ID: install_id, INSTALLATION_DATE: install_time, LOG_LAST_SENTTIME: last_sent_time, LOG_SENT_PERIOD: UPDATE_GAPS, SYSTEM_STATUS: SYSTEM_STATUS_NOT_INITIALIZED, LOGIN_COUNT: 0, ALLOWED_TYPO_LOGIN: allow_typo_login, ALLOWED_UPLOAD: True, ENC_PK: serialize_pk(self._pk), INDEX_J: random.randint(0, WAITLIST_SIZE - 1), }) # Just get the ids of all possible typo candidates for warming # up the cache. perm_index = self._fill_cache_w_garbage() if WARM_UP_CACHE: freq_counts = range(CACHE_SIZE, 0, -1) for i in range(CACHE_SIZE): freq_counts[perm_index[i]] = freq_counts[i] else: freq_counts = [0 for _ in range(CACHE_SIZE)] header_ctx = pkencrypt( self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) logger.info("Initializing the auxiliary data base ({})".format(auxT)) self._aux_tab[HEADER_CTX] = header_ctx # self._aux_tab.create_index(['desc']) self.set_status(SYSTEM_STATUS_ALL_GOOD) # 3. Filling the Typocache with garbage self._fill_waitlist_w_garbage() logger.debug("Initialization Complete") isON = self.get_from_auxtdb(ALLOWED_TYPO_LOGIN) # isON: boolean logger.info("typtop is ON? {}".format(isON))
def init_typtop(self, pw, allow_typo_login=True): """Create the 'typtop' database in user's home-directory. Changes Also, it initializes the required tables as well as the reuired variables, such as, the typo-cache size, the global salt etc. """ logger.info("Initiating typtop db with {}".format( dict(allow_typo_login=allow_typo_login) )) change_db_ownership(self._db_path) # db[auxT].delete() # make sure there's no old unrelevent data # doesn't delete log because it will also be used # whenever a password is changed # *************** Initializing Aux Data ************************* # *************** add org password, its' pks && global salt: ******** # 1. derive public_key from the original password # 2. encrypt the global salt with the enc pk install_id = get_machine_id() install_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) last_sent_time = get_time() self._hmac_salt = os.urandom(SALT_LENGTH) # global salt self._pk, self._sk = generate_key_pair() # ECC key pair self._sk = serialize_sk(self._sk) self._pw = pw self._aux_tab.update({ INSTALLATION_ID: install_id, INSTALLATION_DATE: install_time, LOG_LAST_SENTTIME: last_sent_time, LOG_SENT_PERIOD: UPDATE_GAPS, SYSTEM_STATUS: SYSTEM_STATUS_NOT_INITIALIZED, LOGIN_COUNT: 0, ALLOWED_TYPO_LOGIN: allow_typo_login, ALLOWED_UPLOAD: True, ENC_PK: serialize_pk(self._pk), INDEX_J: random.randint(0, WAITLIST_SIZE-1), }) # Just get the ids of all possible typo candidates for warming # up the cache. perm_index = self._fill_cache_w_garbage() if WARM_UP_CACHE: freq_counts = range(CACHE_SIZE, 0, -1) for i in range(CACHE_SIZE): freq_counts[perm_index[i]] = freq_counts[i] else: freq_counts = [0 for _ in range(CACHE_SIZE)] header_ctx = pkencrypt(self._pk, json.dumps({ REAL_PW: self._pw, HMAC_SALT: urlsafe_b64encode(self._hmac_salt), FREQ_COUNTS: freq_counts })) logger.info("Initializing the auxiliary data base ({})".format(auxT)) self._aux_tab[HEADER_CTX] = header_ctx # self._aux_tab.create_index(['desc']) self.set_status(SYSTEM_STATUS_ALL_GOOD) # 3. Filling the Typocache with garbage self._fill_waitlist_w_garbage() logger.debug("Initialization Complete") isON = self.get_from_auxtdb(ALLOWED_TYPO_LOGIN) # isON: boolean logger.info("typtop is ON? {}".format(isON))