|
| 1 | +"""Simple secret manager which provides an interactive way to input, save, and load credentials for use with pwiki.""" |
| 2 | +import argparse |
1 | 3 | import base64
|
2 |
| -# import re |
3 | 4 | import getpass
|
4 | 5 |
|
5 | 6 | from pathlib import Path
|
| 7 | +from pprint import pprint |
6 | 8 |
|
7 | 9 | _DEFAULT_PX = Path.home() / ".px.txt"
|
8 | 10 |
|
9 |
| -class Wgen: |
10 |
| - """Simple secret manager which provides an interactive way to input, save, and load credentials for use with pwiki.""" |
11 |
| - @staticmethod |
12 |
| - def load_px(px_file: Path = _DEFAULT_PX) -> dict: |
13 |
| - """Loads the specified password file if it exists. Returns a dictionary with username/passwords that were found |
14 |
| -
|
15 |
| - Args: |
16 |
| - px_file (Path, optional): The path to the password file. Defaults to _DEFAULT_PX. |
17 |
| -
|
18 |
| - Raises: |
19 |
| - FileNotFoundError: If a file at Path `px_file` does not exist on the local file system. |
20 |
| -
|
21 |
| - Returns: |
22 |
| - dict: A dict with credentials such that each key is the username and each value is the password. |
23 |
| - """ |
24 |
| - if not px_file.is_file(): |
25 |
| - raise FileNotFoundError(f"'{px_file}' does not exist or is a directory. Did you run Wgen yet? If there is a dir here, rename it first.") |
26 |
| - |
27 |
| - return dict([line.split("\t") for line in base64.b64decode(px_file.read_text().encode()).decode().strip().splitlines()]) |
28 |
| - |
29 |
| - @staticmethod |
30 |
| - def setup(out_file: Path = _DEFAULT_PX, allow_continue: bool = True): |
31 |
| - """Interactively creates a credential save file. |
32 |
| -
|
33 |
| - Args: |
34 |
| - out_file (Path, optional): The path to create the password file at. CAVEAT: If a file exists at this location exists it will be overwritten. Defaults to _DEFAULT_PX. |
35 |
| - allow_continue (bool, optional): Set True to allow user to enter more than one user-pass combo. Defaults to True. |
36 |
| - """ |
37 |
| - pxl = {} |
38 |
| - |
39 |
| - while True: |
40 |
| - print("Please enter the username/password combo(s) you would like to use.") |
41 |
| - u = input("Username: ") |
42 |
| - p = getpass.getpass() |
43 |
| - confirm_p = getpass.getpass("Confirm Password: ") |
44 |
| - |
45 |
| - if p != confirm_p: |
46 |
| - print("ERROR: Entered passwords do not match") |
47 |
| - if Wgen._user_says_no("Try again?"): |
48 |
| - break |
49 |
| - # if not re.match("(?i)(y|yes)", input("Try again? (y/N): ")): |
50 |
| - # break |
51 |
| - else: |
52 |
| - pxl[u] = p |
53 |
| - |
54 |
| - # if not allow_continue or not re.match("(?i)(y|yes)", input("Continue? (y/N): ")): |
55 |
| - if not allow_continue or Wgen._user_says_no("Continue?"): # not re.match("(?i)(y|yes)", input("Continue? (y/N): ")): |
56 |
| - break |
57 |
| - |
58 |
| - if not pxl: |
59 |
| - print("WARNING: You did not make any entries. Doing nothing.") |
60 |
| - return |
61 |
| - |
62 |
| - out_file.write_text(base64.b64encode("\n".join([f"{k}\t{v}" for k, v in pxl.items()]).encode()).decode()) |
63 |
| - print(f"Successfully created '{out_file}'") |
64 |
| - |
65 |
| - @staticmethod |
66 |
| - def _user_says_no(question: str) -> bool: |
67 |
| - """Ask the user a question via interactive command line. |
68 |
| -
|
69 |
| - Args: |
70 |
| - question (str): The question to ask. `" (y/N): "` will be automatically appended to the question. |
71 |
| -
|
72 |
| - Returns: |
73 |
| - bool: True if the user responded with something other than `"y"` or `"yes"`. |
74 |
| - """ |
75 |
| - return input(question + " (y/N): ").strip().lower() not in ("y", "yes") |
| 11 | + |
| 12 | +def load_px(px_file: Path = _DEFAULT_PX) -> dict: |
| 13 | + """Loads the specified password file if it exists. Returns a dictionary with username/passwords that were found |
| 14 | +
|
| 15 | + Args: |
| 16 | + px_file (Path, optional): The path to the password file. Defaults to _DEFAULT_PX. |
| 17 | +
|
| 18 | + Raises: |
| 19 | + FileNotFoundError: If a file at Path `px_file` does not exist on the local file system. |
| 20 | +
|
| 21 | + Returns: |
| 22 | + dict: A dict with credentials such that each key is the username and each value is the password. |
| 23 | + """ |
| 24 | + if not px_file.is_file(): |
| 25 | + raise FileNotFoundError(f"'{px_file}' does not exist or is a directory. Did you run Wgen yet? If there is a directory here, rename it before proceeding.") |
| 26 | + |
| 27 | + return dict([line.split("\t") for line in base64.b64decode(px_file.read_text().encode()).decode().strip().splitlines()]) |
| 28 | + |
| 29 | + |
| 30 | +def setup_px(out_file: Path = _DEFAULT_PX, allow_continue: bool = True, edit_mode: bool = False): |
| 31 | + """Interactively creates a credential save file. |
| 32 | +
|
| 33 | + Args: |
| 34 | + out_file (Path, optional): The path to create the password file at. CAVEAT: If a file exists at this location exists it will be overwritten. Defaults to _DEFAULT_PX. |
| 35 | + allow_continue (bool, optional): Set True to allow user to enter more than one user-pass combo. Defaults to True. |
| 36 | + edit_mode (bool, optional): Enables edit mode, meaning that entries of an existing file will be modified. Does nothing if `out_file` does not exist. Defaults to False. |
| 37 | + """ |
| 38 | + pxl = load_px(out_file) if edit_mode and out_file.is_file() else {} |
| 39 | + |
| 40 | + while True: |
| 41 | + print("Please enter the username/password combo(s) you would like to use.") |
| 42 | + u = input("Username: ") |
| 43 | + p = getpass.getpass() |
| 44 | + confirm_p = getpass.getpass("Confirm Password: ") |
| 45 | + |
| 46 | + if p != confirm_p: |
| 47 | + print("ERROR: Entered passwords do not match") |
| 48 | + if _user_says_no("Try again?"): |
| 49 | + break |
| 50 | + else: |
| 51 | + pxl[u] = p |
| 52 | + |
| 53 | + if not allow_continue or _user_says_no("Continue?"): |
| 54 | + break |
| 55 | + |
| 56 | + if not pxl: |
| 57 | + print("WARNING: You did not make any entries. Doing nothing.") |
| 58 | + return |
| 59 | + |
| 60 | + out_file.write_text(base64.b64encode("\n".join([f"{k}\t{v}" for k, v in pxl.items()]).encode()).decode()) |
| 61 | + print(f"Entries successfully written out to '{out_file}'") |
| 62 | + |
| 63 | + |
| 64 | +def _user_says_no(question: str) -> bool: |
| 65 | + """Ask the user a question via interactive command line. |
| 66 | +
|
| 67 | + Args: |
| 68 | + question (str): The question to ask. `" (y/N): "` will be automatically appended to the question. |
| 69 | +
|
| 70 | + Returns: |
| 71 | + bool: True if the user responded with something other than `"y"` or `"yes"`. |
| 72 | + """ |
| 73 | + return input(question + " (y/N): ").strip().lower() not in ("y", "yes") |
| 74 | + |
| 75 | + |
| 76 | +def main(): |
| 77 | + """Main driver, to be used when this module is invoked via CLI.""" |
| 78 | + cli_parser = argparse.ArgumentParser(description="pwiki Wgen credential manager") |
| 79 | + cli_parser.add_argument('--px-path', type=Path, default=_DEFAULT_PX, dest="px_path", help="The local path to write the password file to") |
| 80 | + cli_parser.add_argument("-e", action='store_true', dest="edit_mode", help="Enables edit/append mode, instead of overwrite (the default)") |
| 81 | + cli_parser.add_argument("--show", action='store_true', help="Read and shows the contents of the px file instead of writing/editing. This overrides the -e option.") |
| 82 | + args = cli_parser.parse_args() |
| 83 | + |
| 84 | + if args.show: |
| 85 | + try: |
| 86 | + pprint(load_px(args.px_path)) |
| 87 | + except FileNotFoundError as e: |
| 88 | + print(e) |
| 89 | + else: |
| 90 | + try: |
| 91 | + setup_px(args.px_path, edit_mode=args.edit_mode) |
| 92 | + except KeyboardInterrupt: |
| 93 | + print("\nkeyboard interrupt, no changes will be made.") |
| 94 | + |
| 95 | + |
| 96 | +if __name__ == "__main__": |
| 97 | + main() |
0 commit comments