merge conflicts
authorSteven Tobin <steventtobin (at) gmail.com>
Mon, 19 Feb 2018 22:59:12 +0000 (22:59 +0000)
committerSteven Tobin <steventtobin (at) gmail.com>
Mon, 19 Feb 2018 22:59:12 +0000 (22:59 +0000)
.gitignore
README.rst
tests/test_xkcdpass.py
xkcdpass/xkcd_password.py

index 7f9bfea33ccd49ff8c73e9185a975f397fac9515..ba40f56eb83802658fd8aace001c0daface0e783 100644 (file)
@@ -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
index d2351609c5d2342eb3a17651c1677b578be0a8f8..152337beb6663cb76d9de4c622a76ccba2f1b24c 100644 (file)
@@ -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
 ==========
index 9ae1f7cc8c27cdd228fcd49b4cb1f70e66faac3d..dd3ecccd591ad3a6eb5e9deeceebc288f216c2fc 100644 (file)
@@ -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)
index 06f6c238677b57ae199a056e1f053f88b3ffeb87..ba8a053a2b14f66d56b06e76e3e4d0a1ad61427d 100755 (executable)
@@ -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,
git clone https://git.99rst.org/PROJECT