Last active
April 7, 2018 12:54
-
-
Save s-c-p/6d57dd7ff52740afdf7e2542f68eeea9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import copy | |
import hashlib | |
from lesspass__renderPwd import render_password | |
DEFAULT_PROFILE = { | |
"site": "", | |
"login": "", | |
"options": { | |
"uppercase": True, | |
"lowercase": True, | |
"digits": True, | |
"symbols": True, | |
"length": 16, | |
"counter": 1 | |
}, | |
"crypto": { | |
"method": "pbkdf2", | |
"iterations": 100000, | |
"keylen": 32, | |
"digest": "sha256" | |
} | |
} | |
import collections | |
dictType = collections.Mapping | |
def get_target_dct(dct, locations): | |
if len(locations) > 1: | |
k, *remaining = locations | |
return get_target_dct(dct[k], remaining) | |
else: | |
return dct[locations.pop()] | |
class UpdateDict: | |
def __init__(self, master_dict): | |
self._u = set() | |
self.dct = copy.deepcopy(master_dict) | |
self._check_uniqueness(self.dct) | |
return | |
def _check_uniqueness(self, dct): | |
for k, v in dct.items(): | |
if k in self._u: | |
warn | |
else: | |
self._u.add(k) | |
if isinstance(v, dictType): | |
self._check_uniqueness(v) | |
return | |
def _update(self, *keys_and_val): | |
""" | |
>>> l = dict(zip(list('abc'), range(3))) | |
>>> l['b'] = dict(zip(list('abc'), range(3))) | |
>>> l['b']['c'] = dict(zip(list('abc'), range(3))) | |
>>> l['b']['c']['a'] = 100 | |
(b, c, a, 100) | |
""" | |
if len(xxx) < 2: | |
raise NotEnoughInfo | |
value, *location = xxx[::-1] | |
location.reverse() | |
final_key = location.pop() | |
ptr__target_dct = get_target_dct(location) | |
ptr__target_dct[final_key] = value | |
return | |
def updates(self, *args): | |
if isinstance(args, list): | |
for an_update in args: | |
self._update(an_update) | |
else: | |
self._update(args) | |
return self.dct | |
def generate_password(user_prefrences, master_password): | |
final_profile = copy.deepcopy(DEFAULT_PROFILE) | |
final_profile.update(user_prefrences) | |
entropy = _calc_entropy(final_profile, master_password) | |
del master_password | |
lesspass_password = render_password(entropy, final_profile['options']) | |
return lesspass_password | |
def _calc_entropy(profile, master_password): | |
salt = profile['site'] + profile['login'] + \ | |
str( int(str(profile['options']['counter']), 16) ) | |
entropy = hashlib.pbkdf2_hmac( | |
password = master_password.encode('utf-8'), | |
salt = salt.encode('utf-8'), | |
dklen = profile['crypto']['keylen'], | |
hash_name = profile['crypto']['digest'], | |
iterations = profile['crypto']['iterations'] | |
) | |
return entropy.hex() | |
def createFingerprint(string): | |
import hmac | |
hr = hmac.new(string.encode('utf-8'), digestmod="sha256") | |
return hr.digest().hex() | |
def isSupported(): | |
try: | |
simple_profile = copy.deepcopy(DEFAULT_PROFILE) | |
simple_profile.update({'crypto': {'iterations': 1 }}) | |
x = generate_password(simple_profile, "LessPass") | |
assert x == "n'LTsjPA#3E$e*2'" | |
except Exception as e: | |
pass | |
def test_calc_entropy(): | |
master_password = "password" | |
test_profile = copy.deepcopy(DEFAULT_PROFILE) | |
# testing line 5 | |
test_profile.update({ | |
"site": "example.org", | |
"login": "[email protected]" | |
}) | |
test_val = _calc_entropy(test_profile, master_password) | |
assert test_val == "dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e" | |
# preparing for test @ line 50 | |
p1 = copy.deepcopy(test_profile) | |
p2 = copy.deepcopy(test_profile) | |
# done preparing for test @ line 50 | |
# testing line 29 | |
test_profile["crypto"].update({ | |
"iterations": 8192, | |
"digest": "sha512", | |
"keylen": 16 | |
}) | |
test_val = _calc_entropy(test_profile, master_password) | |
assert test_val == "fff211c16a4e776b3574c6a5c91fd252" | |
# testing line 50 | |
p1["options"].update({ | |
"counter": 1 | |
}) | |
t1 = _calc_entropy(p1, master_password) | |
p2["options"].update({ | |
"counter": 2 | |
}) | |
t2 = _calc_entropy(p2, master_password) | |
assert t1 != t2 | |
return | |
def test_generate_password(): | |
master_password = "password" | |
test_profile = copy.deepcopy(DEFAULT_PROFILE) | |
test_profile.update({ | |
"site": "example.org", | |
"login": "[email protected]" | |
}) | |
test_pwd = generate_password(test_profile, master_password) | |
assert test_pwd == "WHLpUL)e00[iHR+w" | |
test_profile["options"].update({ | |
"counter": 2, | |
"symbols": False, | |
"length": 14 | |
}) | |
test_pwd = generate_password(test_profile, master_password) | |
assert test_pwd == "MBAsB7b1Prt8Sl" | |
assert len(test_pwd) == 14 | |
return | |
def test_createFingerprint(): | |
assert createFingerprint("password") == "e56a207acd1e6714735487c199c6f095844b7cc8e5971d86c003a7b6f36ef51e" | |
if __name__ == '__main__': | |
from tester import run_tests | |
run_tests() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import getpass | |
# RENDER PASSWORD ------------------------------------------------------------ | |
import string | |
from collections import namedtuple | |
TransPassword = namedtuple("TransPassword", ["value", "entropy"]) | |
CHAR_SET = { | |
"digits": string.digits, | |
"symbols": string.punctuation, | |
"uppercase": string.ascii_uppercase, | |
"lowercase": string.ascii_lowercase | |
} | |
def _get_active_rules(password_profile): | |
ORIGINAL_LESSPASS_S_RULE_FILTERATION_ORDER = \ | |
['lowercase', 'uppercase', 'digits', 'symbols'] | |
answer = list() | |
for a_rule in ORIGINAL_LESSPASS_S_RULE_FILTERATION_ORDER: | |
if password_profile[a_rule] is True: | |
answer.append(a_rule) | |
return answer | |
def _get_char_set(rules): | |
ans = str() | |
for a_rule in rules: | |
ans += CHAR_SET[a_rule] | |
return ans | |
def _getOneCharPerRule(entropy, rules): | |
ocpr = str() | |
for rule in rules: | |
password = consume_entropy("", entropy, CHAR_SET[rule], 1) | |
ocpr += password.value | |
entropy = password.entropy | |
return TransPassword(ocpr, entropy) | |
def _insertStringPseudoRandomly(generatedPassword, entropy, string): | |
for char in string: | |
quotient, remainder = divmod(entropy, len(generatedPassword)) | |
generatedPassword = generatedPassword[0:remainder] + \ | |
char + generatedPassword[remainder:] | |
entropy = quotient | |
return generatedPassword | |
def consume_entropy(generatedPassword, quotient, valid_chars, max_len): | |
if len(generatedPassword) >= max_len: | |
return TransPassword(generatedPassword, quotient) | |
quotient, remainder = divmod(quotient, len(valid_chars)) | |
generatedPassword += valid_chars[remainder] | |
return consume_entropy( | |
generatedPassword, quotient, valid_chars, max_len | |
) | |
def render_password(entropy, options): | |
rules = _get_active_rules(options) | |
char_set = _get_char_set(rules) | |
password = consume_entropy( | |
generatedPassword = "", | |
valid_chars = char_set, | |
quotient = int(entropy, 16), | |
max_len = options["length"] - len(rules) | |
) | |
chars_to_add = _getOneCharPerRule(password.entropy, rules) | |
ans = _insertStringPseudoRandomly( | |
generatedPassword = password.value, | |
entropy = chars_to_add.entropy, | |
string = chars_to_add.value | |
) | |
return ans | |
# core ----------------------------------------------------------------------- | |
import copy | |
import hashlib | |
DEFAULT_PROFILE = { | |
"site": "", | |
"login": "", | |
"options": { | |
"uppercase": True, | |
"lowercase": True, | |
"digits": True, | |
"symbols": True, | |
"length": 16, | |
"counter": 1 | |
}, | |
"crypto": { | |
"method": "pbkdf2", | |
"iterations": 100000, | |
"keylen": 32, | |
"digest": "sha256" | |
} | |
} | |
def generate_password(user_prefrences, master_password): | |
final_profile = copy.deepcopy(DEFAULT_PROFILE) | |
final_profile.update(user_prefrences) | |
entropy = _calc_entropy(final_profile, master_password) | |
del master_password | |
lesspass_password = render_password(entropy, final_profile['options']) | |
return lesspass_password | |
def _calc_entropy(profile, master_password): | |
salt = profile['site'] + profile['login'] + \ | |
str( int(str(profile['options']['counter']), 16) ) | |
entropy = hashlib.pbkdf2_hmac( | |
password = master_password.encode('utf-8'), | |
salt = salt.encode('utf-8'), | |
dklen = profile['crypto']['keylen'], | |
hash_name = profile['crypto']['digest'], | |
iterations = profile['crypto']['iterations'] | |
) | |
return entropy.hex() | |
def test_generate_password(): | |
master_password = "password" | |
test_profile = copy.deepcopy(DEFAULT_PROFILE) | |
test_profile.update({ | |
"site": "example.org", | |
"login": "[email protected]" | |
}) | |
test_pwd = generate_password(test_profile, master_password) | |
assert test_pwd == "WHLpUL)e00[iHR+w" | |
test_profile["options"].update({ | |
"counter": 2, | |
"symbols": False, | |
"length": 14 | |
}) | |
test_pwd = generate_password(test_profile, master_password) | |
assert test_pwd == "MBAsB7b1Prt8Sl" | |
assert len(test_pwd) == 14 | |
return | |
# ---------------------------------------------------------------------------- | |
def get_opts(): | |
ans = dict() | |
curr = DEFAULT_PROFILE["options"] | |
print("* Password contains uppercase chars") | |
if input("press n to remove uppercase: ") in ['n', 'N']: | |
ans['uppercase'] = False | |
print("* Password contains lowercase") | |
if input("press n to remove lowercase: ") in ['n', 'N']: | |
ans['lowercase'] = False | |
print("* Password contains digits") | |
if input("press n to remove digits: ") in ['n', 'N']: | |
ans['digits'] = False | |
print("* Password contains symbols") | |
if input("press n to remove symbols: ") in ['n', 'N']: | |
ans['symbols'] = False | |
i = input("* Password length (default 16): ") | |
try: int(i) | |
except: pass | |
else: ans['length'] = i | |
i = input("* Password iteration (default 1st): ") | |
try: int(i) | |
except: pass | |
else: ans['counter'] = i | |
return ans | |
def main(): | |
args = sys.argv[1:] | |
try: | |
site, login = args | |
except IndexError: | |
return | |
print("Site: ", site) | |
print("Login: ", login) | |
i = input( | |
""" | |
Press | |
[q] to quit and start over | |
[f] to fine tune settings | |
or just [Enter] to proceed | |
""" | |
) | |
if i in ["q", "Q"]: | |
return | |
session_pref = copy.deepcopy(DEFAULT_PROFILE) | |
session_pref.update({ | |
"site": site, | |
"login": login | |
}) | |
if i in ['f', 'F']: | |
session_pref["options"].update(get_opts()) | |
master_password = getpass.getpass("Enter master password:\n") | |
ans = generate_password(session_pref, master_password) | |
del master_password | |
print("Generate password:") | |
print(ans) | |
return | |
if __name__ == '__main__': | |
test_generate_password() | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Lesspass's render password module implemented in python | |
this file tries to mimic index.js, all necessary functions are defined in this | |
file itself | |
""" | |
import string | |
from collections import namedtuple | |
TransPassword = namedtuple("TransPassword", ["value", "entropy"]) | |
############################################################################## | |
# file: chars.js | |
############################################################################## | |
CHAR_SET = { | |
"digits": string.digits, | |
"symbols": string.punctuation, | |
"uppercase": string.ascii_uppercase, | |
"lowercase": string.ascii_lowercase | |
} | |
def _get_active_rules(password_profile): | |
# earlier it was just-- | |
# rules = list(filter(lambda k: options[k] is True, options)) | |
# but I was getting strange bugs, first because | |
# Py's dict key odering problem and the fact illustrated below | |
ORIGINAL_LESSPASS_S_RULE_FILTERATION_ORDER = \ | |
['lowercase', 'uppercase', 'digits', 'symbols'] | |
# had to give this eye-catchy name because this ARBITARY order decided | |
# by lesspass authors decides order in which chars appear in | |
# `valid_chars` of render_password | |
answer = list() | |
for a_rule in ORIGINAL_LESSPASS_S_RULE_FILTERATION_ORDER: | |
if password_profile[a_rule] is True: | |
answer.append(a_rule) | |
return answer | |
def _get_char_set(rules): | |
ans = str() | |
for a_rule in rules: | |
ans += CHAR_SET[a_rule] | |
return ans | |
def _getOneCharPerRule(entropy, rules): | |
ocpr = str() | |
for rule in rules: | |
password = consume_entropy("", entropy, CHAR_SET[rule], 1) | |
ocpr += password.value | |
entropy = password.entropy | |
return TransPassword(ocpr, entropy) | |
def _insertStringPseudoRandomly(generatedPassword, entropy, string): | |
for char in string: | |
quotient, remainder = divmod(entropy, len(generatedPassword)) | |
generatedPassword = generatedPassword[0:remainder] + \ | |
char + generatedPassword[remainder:] | |
entropy = quotient | |
return generatedPassword | |
def test_getOneCharPerRule(): | |
# test on-- https://bit.ly/2uxYPAg | |
test_val = _getOneCharPerRule(26*26, ["lowercase", "uppercase"]) | |
assert test_val.value[:2] == "aA" | |
assert len(test_val.value) == 2 | |
assert test_val.entropy == 1 | |
return | |
def test_insertStringPseudoRandomly(): | |
# test on-- https://bit.ly/2GJIBcg | |
test_val = _insertStringPseudoRandomly("123456", 7*6 + 2, "uT") | |
assert test_val == "T12u3456" | |
return | |
############################################################################## | |
# file: entropy.js | |
############################################################################## | |
def consume_entropy(generatedPassword, quotient, valid_chars, max_len): | |
if len(generatedPassword) >= max_len: | |
return TransPassword(generatedPassword, quotient) | |
quotient, remainder = divmod(quotient, len(valid_chars)) | |
generatedPassword += valid_chars[remainder] | |
return consume_entropy( | |
generatedPassword, quotient, valid_chars, max_len | |
) | |
def test_consume_entropy(): | |
# test on-- https://bit.ly/2GruTrj | |
test_val = consume_entropy("", 4*4 + 2, "abcd", 2) | |
assert test_val.value == "ca" | |
assert test_val.entropy == 1 | |
return | |
############################################################################## | |
# file: index.js | |
############################################################################## | |
def render_password(entropy, options): | |
# see-- return of `https://bit.ly/2pZCCq3` is hexadecimal str only | |
rules = _get_active_rules(options) | |
char_set = _get_char_set(rules) | |
password = consume_entropy( | |
generatedPassword = "", | |
valid_chars = char_set, | |
quotient = int(entropy, 16), # BUG: possibly, cuz quot... is now decimal not 0xDEADBEEF kinda stuff | |
max_len = options["length"] - len(rules) | |
) | |
chars_to_add = _getOneCharPerRule(password.entropy, rules) | |
ans = _insertStringPseudoRandomly( | |
generatedPassword = password.value, | |
entropy = chars_to_add.entropy, | |
string = chars_to_add.value | |
) | |
return ans | |
def test_render_password(): | |
# fails test-- https://bit.ly/2H11eGv | |
test_options = { | |
"length": 16, | |
"lowercase": True, | |
"uppercase": True, | |
"digits": True, | |
"symbols": True | |
} | |
test_entropy = "dc33d431bce2b01182c613382483ccdb0e2f66482cbba5e9d07dab34acc7eb1e" | |
check_value = render_password(test_entropy, test_options) | |
assert check_value[0] == "W" | |
assert check_value[1] == "H" | |
assert len(check_value) == 16 | |
test_options.update({"length": 20}) | |
check_value = render_password(test_entropy, test_options) | |
assert len(check_value) == 20 | |
test_options.update({"length": 6}) | |
check_value = render_password(test_entropy, test_options) | |
assert any([x.islower() for x in check_value]) | |
assert any([x.isupper() for x in check_value]) | |
assert any([x.isdigit() for x in check_value]) | |
assert all(x for x in check_value if x in string.punctuation) | |
return | |
if __name__ == '__main__': | |
from tester import run_tests | |
run_tests() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import inspect | |
def run_tests(): | |
fg = inspect.currentframe().f_back.f_globals | |
g = list(fg.keys()) | |
for name in g: | |
func = fg[name] | |
if callable(func) and name.startswith("test"): | |
print(name, end=" ") | |
try: | |
func() | |
except Exception as e: | |
print("failed", end="\n\t") | |
print(repr(e)) | |
else: | |
print("passed") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment