From: Steven Tobin Date: Mon, 19 Feb 2018 22:59:12 +0000 (+0000) Subject: merge conflicts X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=dbbf1f15bf908aadbf16f9503b46784d11bd2d1c;p=redacted-XKCD-password-generator.git merge conflicts --- diff --git a/.gitignore b/.gitignore index 7f9bfea..ba40f56 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ nosetests.xml .pydevproject # VS Code -.vscode \ No newline at end of file +.vscode + +# mypy +.mypy_cache \ No newline at end of file diff --git a/README.rst b/README.rst index d235160..152337b 100644 --- a/README.rst +++ b/README.rst @@ -103,6 +103,15 @@ A concise overview of the available ``xkcdpass`` options can be accessed via:: separator character between words -s SEP, --separator SEP Separate generated passphrases with SEP. + -C CASE, --case CASE + Choose the method for setting the case of each word in + the passphrase. Choices: ['alternating', 'upper', + 'lower', 'random'] (default: 'lower'). + --allow-weak-rng + Allow fallback to weak RNG if the system does not + support cryptographically secure RNG. Only use this if + you know what you are doing. + Word lists ========== diff --git a/tests/test_xkcdpass.py b/tests/test_xkcdpass.py index 9ae1f7c..dd3eccc 100644 --- a/tests/test_xkcdpass.py +++ b/tests/test_xkcdpass.py @@ -32,7 +32,7 @@ class XkcdPasswordTests(unittest.TestCase): self.assertEqual("".join(map(lambda x: x[0], result.split())), word) def test_commandlineCount(self): - count = 5 + count = 6 result = subprocess.check_output([ sys.executable, "xkcdpass/xkcd_password.py", "-w", WORDFILE, @@ -65,6 +65,41 @@ class XkcdPasswordTests(unittest.TestCase): "--separator", ""]) self.assertEqual(result.find(b"\n"), -1) + def test_set_case(self): + words = "this is only a test".lower().split() + words_before = set(words) + + results = {} + + results["lower"] = xkcd_password.set_case(words, method="lower") + results["upper"] = xkcd_password.set_case(words, method="upper") + results["alternating"] = xkcd_password.set_case(words, method="alternating") + results["random"] = xkcd_password.set_case(words, method="random", testing=True) + + words_after = set([word.lower() for group in list(results.values()) for word in group]) + + # Test that no words have been fundamentally mutated by any of the methods + self.assertTrue(words_before == words_after) + + # Test that the words have been uppered or lowered respectively. + self.assertTrue(all([word.islower() for word in results["lower"]])) + self.assertTrue(all([word.isupper() for word in results["upper"]])) + + # Test that the words have been correctly uppered randomly. + expected_random_result_1 = ['THIS', 'IS', 'ONLY', 'a', 'test'] + expected_random_result_2 = ['THIS', 'IS', 'a', 'test', 'ALSO'] + + words_extra = "this is a test also".lower().split() + observed_random_result_1 = results["random"] + observed_random_result_2 = xkcd_password.set_case( + words_extra, + method="random", + testing=True + ) + + self.assertTrue(expected_random_result_1 == observed_random_result_1) + self.assertTrue(expected_random_result_2 == observed_random_result_2) + if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(XkcdPasswordTests) diff --git a/xkcdpass/xkcd_password.py b/xkcdpass/xkcd_password.py index 06f6c23..ba8a053 100755 --- a/xkcdpass/xkcd_password.py +++ b/xkcdpass/xkcd_password.py @@ -222,11 +222,80 @@ def try_input(prompt, validate): return validate(answer) +def alternating_case(words): + """ + Set EVERY OTHER word to UPPER case. + """ + return [word.upper() + if i % 2 == 0 + else word + for i, word in enumerate(lower_case(words))] + + +def upper_case(words): + """ + Set ALL words to UPPER case. + """ + return [w.upper() for w in words] + + +def lower_case(words): + """ + Set ALL words to LOWER case. + """ + return [w.lower() for w in words] + + +def random_case(words, testing=False): + """ + Set RANDOM words to UPPER case. + """ + def make_upper(word): + """Return 'word.upper()' on a random basis.""" + if testing: + random.seed(word) + + if random.choice([True, False]): + return word.upper() + else: + return word + + return [make_upper(word) for word in lower_case(words)] + + +CASE_METHODS = {"alternating": alternating_case, + "upper": upper_case, + "lower": lower_case, + "random": random_case} + + +def set_case(words, method="lower", testing=False): + """ + Perform capitalization on some or all of the strings in `words`. + + Default method is "lower". + + Args: + words (list): word list generated by `choose_words()` or `find_acrostic()`. + method (str): one of {"alternating", "upper", "lower", "random"}. + testing (bool): only affects method="random". + If True: the random seed will be set to each word prior + to choosing True or False before setting the case to upper. + This way we can test that random is working by giving different + word lists. + """ + if (method == "random") and (testing): + return random_case(words, testing=True) + else: + return CASE_METHODS[method](words) + + def generate_xkcdpassword(wordlist, numwords=6, interactive=False, acrostic=False, - delimiter=" "): + delimiter=" ", + case="lower"): """ Generate an XKCD-style password from the words in wordlist. """ @@ -242,8 +311,8 @@ def generate_xkcdpassword(wordlist, words = choose_words(wordlist, numwords) else: words = find_acrostic(acrostic, worddict) - - return delimiter.join(words) + + return delimiter.join(set_case(words, method=case)) # useful if driving the logic from other code if not interactive: @@ -299,7 +368,8 @@ def emit_passwords(wordlist, options): interactive=options.interactive, numwords=options.numwords, acrostic=options.acrostic, - delimiter=options.delimiter + delimiter=options.delimiter, + case=options.case, ), end=options.separator) count -= 1 @@ -367,6 +437,12 @@ class XkcdPassArgumentParser(argparse.ArgumentParser): "-s", "--separator", dest="separator", default="\n", metavar="SEP", help="Separate generated passphrases with SEP.") + self.add_argument( + "-C", "--case", + dest="case", type=str, metavar="CASE", + choices=list(CASE_METHODS.keys()), default="lower", + help="Choose the method for setting the case of each word in the passphrase. " + "Choices: {cap_meths} (default: 'lower').".format(cap_meths=list(CASE_METHODS.keys()))) self.add_argument( "--allow-weak-rng", action="store_true", dest="allow_weak_rng", default=False,