Skip to content

Commit 6e4a40f

Browse files
committed
release 0.2: Decouple crypto from client, change cli args, return objects instead of single secret.
1 parent e005514 commit 6e4a40f

File tree

2 files changed

+106
-108
lines changed

2 files changed

+106
-108
lines changed

src/pw_client.py

Lines changed: 32 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import pw_utils
1111
from pw_encryption import SynchronousEncryption
12-
from pw_config import ENCRYPTION_KEY
1312

1413

1514
class PasswordClient:
@@ -18,27 +17,23 @@ def __init__(self, creds_dir: str, creds_file_path: str):
1817
self.creds_file_path = creds_file_path
1918
self.pw_dict = pw_utils.get_pws_from_json_file(creds_file_path)
2019
# TODO: Move crypto to the command
21-
self.crypto = SynchronousEncryption(ENCRYPTION_KEY)
2220

23-
def get_pw(self, entity: str, attribute: str = None, section: str = None) -> dict:
24-
"""Get data from self.pw_dict, copy to clipboard and print to console.
21+
def get_secrets_data(self, entity: str, section: str = None) -> dict:
22+
"""Return secrets data from self.pw_dict.
2523
2624
Args:
27-
entity (str): The name of the holder of the password, e.g. "GitHub".
28-
attribute (str, optional):
29-
Defaults to "password". Adjust if you want to retrieve "username"
30-
or "website".
25+
entity (str):
26+
The name of the holder of the password, e.g. "GitHub".
3127
section (str, optional):
3228
Defaults to "main". Adjust if you want to access data from an
3329
other section.
3430
"""
35-
if attribute is None:
36-
attribute = 'password'
3731
if section is None:
3832
section = 'main'
39-
pw_data = self.pw_dict[section][entity]
40-
return pw_data
33+
secrets_data = self.pw_dict[section][entity]
34+
return secrets_data
4135

36+
# TODO: Move generate random pw to utils
4237
@staticmethod
4338
def generate_random_password(special_characters=True,
4439
password_length: int = 42) -> str:
@@ -50,24 +45,28 @@ def generate_random_password(special_characters=True,
5045
random_password = ''.join(random.choice(characters) for i in range(password_length))
5146
return random_password
5247

53-
def add_new_pw(self, entity: str, password: str = None, username: str = None,
54-
website: str = None, section: 'str' = None,
55-
encryption: bool = True) -> None:
56-
"""Adds a new password to the password file.
48+
def add_new_secrets_data(self, entity: str, secrets_data: dict,
49+
section: 'str' = None, overwrite: bool = False) -> bool:
50+
"""Adds new secrets data to the password file.
5751
5852
If the a password already exists in the creds.json file, this method will update it.
5953
6054
Args:
6155
entity (str): The entity that you need the password for, e.g. "GitHub"
62-
password (str, optional): If you want, you can specify a password. If you leave it,
63-
it will generate a random password with 42 characters.
64-
username (str, optional): Pass a user name if you want to add it to the json file.
65-
website (str, optional): Pass a website if you want to add it to the json file.
66-
section (str, optional): You may set the section of the json file to which this
67-
password will be added to. Defaults to the section "main".
68-
encryption (bool, defaults to True):
69-
If true this function will decrypt the password before saving it. The function
70-
will save the password without encryption if this arg is set to false.
56+
secrets_data (dict):
57+
A dictionary that you want to store and that holds your
58+
secrets. Example:
59+
{'password': 'p4ssw0rd',
60+
'website': 'https://kuda.ai',
61+
'user_name': 'DataDave'}
62+
section (str, optional):
63+
You may set the section of the json file to which this
64+
password will be added to. Defaults to the section "main".
65+
overwrite (bool, optional):
66+
If set to True, an existing password will be overwritten.
67+
68+
Returns:
69+
bool: True for writing new data, False if no action.
7170
"""
7271
self.create_backup()
7372

@@ -77,23 +76,19 @@ def add_new_pw(self, entity: str, password: str = None, username: str = None,
7776
if not self.check_existence_of_section(section):
7877
self.create_section(section)
7978

80-
if password is None:
81-
password = self.generate_random_password()
82-
pyperclip.copy(password)
79+
if entity in self.pw_dict[section] and overwrite is False:
80+
print('Entity is already there. Nothing happened.')
81+
print('Use the -ow / --overwrite option to update existing secrets data.')
82+
return False
8383

84-
if encryption:
85-
password = self.crypto.encrypt(password)
86-
87-
new_password = {entity: {}}
88-
new_password[entity]['password'] = password
89-
new_password[entity]['username'] = username if username else 'not specified'
90-
new_password[entity]['website'] = website if website else 'not specified'
84+
new_secrets_data = {entity: secrets_data}
9185

9286
print(f'Created new password for "{entity}".')
9387
print('')
9488

95-
self.pw_dict[section].update(new_password)
89+
self.pw_dict[section].update(new_secrets_data)
9690
self.save_dict_to_file()
91+
return True
9792

9893
def create_backup(self):
9994
"""Create a backup of the dictionary with the passwords."""
@@ -133,7 +128,7 @@ def remove_section(self, section):
133128
self.save_dict_to_file()
134129
print(f'Removed Section: "{section}"')
135130

136-
def remove_password(self, entity: str, section: str = None) -> None:
131+
def remove_entity(self, entity: str, section: str = None) -> None:
137132
"""Removes a password from the creds.json file."""
138133
section = section or 'main'
139134
self.pw_dict[section].pop(entity)
@@ -161,13 +156,3 @@ def manipulate_passwords(self, crypto: Callable = None) -> None:
161156
manipulated_password = crypto(current_password)
162157
v["password"] = manipulated_password
163158
self.save_dict_to_file()
164-
165-
def encrypt_single_string(self, text: str) -> str:
166-
return self.crypto.encrypt(text)
167-
168-
def encrypt_all_passwords(self):
169-
self.create_backup()
170-
self.manipulate_passwords(self.crypto.encrypt)
171-
172-
def decrypt_all_passwords(self):
173-
self.manipulate_passwords(self.crypto.decrypt)

src/pw_command.py

Lines changed: 74 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
#!/opt/homebrew/Caskroom/miniconda/base/bin/python
2-
"""
3-
Execute this command to get pw from json into clipboard.
4-
Type "pw -h" to get help on all available commands.
5-
Basic usage: "pw <entity>" -> Gets password of entity from main section.
6-
7-
The creds.json file should look like this:
8-
9-
"section": {
10-
"entity": {
11-
"password": "lorem",
12-
"username": "ipsum",
13-
"additional_info": "...",
14-
"website": "www.kuda.ai"
15-
}
16-
}
17-
"""
182
import argparse
193

204
import pyperclip
215

22-
from pw_config import CREDS_DIR, CREDS_FILE_PATH
6+
from pw_config import CREDS_DIR, CREDS_FILE_PATH, ENCRYPTION_KEY
237
import pw_client
248
from pw_encryption import SynchronousEncryption
25-
from pw_config import ENCRYPTION_KEY
269

2710
HELP_TEXT = {
28-
'input': 'Name of entity that holds the password.',
11+
'entity': 'Name of entity that holds the password.',
2912
'all_sections': 'Print all available sections.',
3013
'section':
3114
'''Pass a section to print all entities of that section.
@@ -45,17 +28,27 @@
4528
}
4629

4730

31+
# TODO: Can I attach a callable / function directly to arg parse so
32+
# TODO: that I can avoid the exhaustingly long if / else statements below?
4833
def parse_args():
4934
parser = argparse.ArgumentParser(description='Manage your passwords from your terminal.')
50-
parser.add_argument('input', type=str, help=HELP_TEXT['input'], nargs='?')
35+
parser.add_argument('entity', type=str, help=HELP_TEXT['entity'], nargs='?')
36+
37+
parser.add_argument('-k', '--secret_key', type=str, default='password')
38+
parser.add_argument('-ks', '--available_keys', action='store_true')
5139
parser.add_argument('-as', '--all_sections', action='store_true', help=HELP_TEXT['all_sections'])
5240
parser.add_argument('-s', '--section', type=str, help=HELP_TEXT['section'])
5341
parser.add_argument('-r', '--generate_random_pw', action='store_true', help=HELP_TEXT['generate_random_pw'])
54-
parser.add_argument('-n', '--add_new_password', type=str, help=HELP_TEXT['add_new_password'])
42+
parser.add_argument('-rl', '--random_password_length', type=int, default=42)
43+
44+
parser.add_argument('-n', '--new_secrets_data', type=str, help=HELP_TEXT['add_new_password'])
45+
parser.add_argument('-pw', '--set_password', type=str, help=HELP_TEXT['set_password'])
5546
parser.add_argument('-u', '--username', type=str)
5647
parser.add_argument('-w', '--website', type=str)
57-
parser.add_argument('-pw', '--set_password', type=str, help=HELP_TEXT['set_password'])
58-
parser.add_argument('-rm', '--remove_password', type=str, help=HELP_TEXT['remove'])
48+
parser.add_argument('--kwargs', '--keyword_arguments', type=str)
49+
parser.add_argument('-ow', '--overwrite', action='store_true')
50+
51+
parser.add_argument('-rm', '--remove_entity', type=str, help=HELP_TEXT['remove'])
5952
parser.add_argument('-rms', '--remove_section', type=str, help=HELP_TEXT['remove'])
6053

6154
args = parser.parse_args()
@@ -67,67 +60,88 @@ def main():
6760
crypto = SynchronousEncryption(ENCRYPTION_KEY)
6861
pw = pw_client.PasswordClient(CREDS_DIR, CREDS_FILE_PATH)
6962

70-
# TODO: Get data inside pw_command.py
71-
# pw_data = pw.get_pw(???)
72-
7363
if args.all_sections:
7464
return pw.print_sections()
7565

76-
if args.add_new_password:
66+
if args.new_secrets_data:
67+
secrets_data = {}
68+
7769
if args.set_password:
78-
encrypted_password = crypto.encrypt(args.set_password)
70+
new_password = args.set_password
7971
else:
80-
new_random_password = pw.generate_random_password()
81-
encrypted_password = crypto.encrypt(new_random_password)
82-
return pw.add_new_pw(entity=args.add_new_password, username=args.username,
83-
website=args.website, section=args.section,
84-
password=encrypted_password)
85-
86-
if args.username:
87-
decrypted_username = pw.get_pw(args.username, 'username', args.section)
88-
encrypted_username = crypto.encrypt(decrypted_username)
89-
return encrypted_username
90-
91-
if args.website:
92-
return pw.get_pw(args.website, 'website', args.section)
93-
94-
if args.remove_password:
95-
return pw.remove_password(entity=args.remove_password, section=args.section)
72+
new_password = pw.generate_random_password(password_length=args.random_password_length)
73+
encrypted_password = crypto.encrypt(new_password)
74+
secrets_data['password'] = encrypted_password
75+
76+
if args.username:
77+
encrypted_username = crypto.encrypt(args.username)
78+
secrets_data['username'] = encrypted_username
79+
80+
if args.website:
81+
encrypted_website = crypto.encrypt(args.website)
82+
secrets_data['website'] = encrypted_website
83+
84+
if args.kwargs:
85+
kwargs_as_list = args.kwargs.split(',')
86+
for kwarg in kwargs_as_list:
87+
if len(kwarg.split('=')) > 2:
88+
print('There is something wrong with your kwargs ...')
89+
print('Desired format:')
90+
print('"pw --kwargs brand=fender,guitar=strat,string_gauge=0.10"')
91+
print('')
92+
key, value = kwarg.split('=')
93+
encrypted_value = crypto.encrypt(value)
94+
secrets_data[key] = encrypted_value
95+
96+
pw.add_new_secrets_data(entity=args.new_secrets_data,
97+
secrets_data=secrets_data,
98+
section=args.section,
99+
overwrite=args.overwrite)
100+
101+
return True
102+
103+
if args.remove_entity:
104+
return pw.remove_entity(entity=args.remove_entity, section=args.section)
96105

97106
if args.remove_section:
98107
return pw.remove_section(args.remove_section)
99108

100-
if args.section and args.input:
101-
encrypted_password = pw.get_pw(entity=args.input, section=args.section)
102-
password = crypto.decrypt(encrypted_password)
103-
return password
104-
109+
# TODO: Move logic of "create new section if not exists" to client
105110
if args.section:
106111
try:
107112
return pw.print_keys_of_section(args.section)
108113
except KeyError:
109114
return pw.create_section(args.section)
110115

116+
# TODO: Move function "generate_random_pw" from pw_client to utils
111117
if args.generate_random_pw:
112-
if args.input is not None:
113-
random_pw = pw.generate_random_password(password_length=int(args.input))
118+
if args.entity is not None:
119+
random_pw = pw.generate_random_password(password_length=args.random_password_length)
114120
else:
115-
random_pw = pw.generate_random_password()
121+
random_pw = pw.generate_random_password(password_length=args.random_password_length)
116122
pyperclip.copy(random_pw)
117123
print('The random password has been copied into your clipboard.')
118124
print('')
119125
return random_pw
120126

121-
if args.input is None:
122-
return print('Nothing happened. No flags used. No args passed after pw command.')
127+
if args.entity is None:
128+
print('Nothing happened. No flags used. No args passed after pw command.')
129+
return False
123130

124-
if args.input:
125-
encrypted_password = pw.get_pw(args.input)
126-
decrypted_password = crypto.decrypt(encrypted_password)
127-
pyperclip.copy(decrypted_password)
128-
print(f'Copied {attribute} for "{entity}" into your clipboard.')
131+
secrets_data = pw.get_secrets_data(entity=args.entity, section=args.section)
132+
133+
if args.available_keys:
134+
print(f'There are {len(secrets_data.keys())} available keys for {args.entity}:')
135+
print(', '.join(secrets_data.keys()))
136+
return True
137+
138+
if args.entity:
139+
encrypted_secret_value = secrets_data[args.secret_key]
140+
decrypted_secret_value = crypto.decrypt(encrypted_secret_value)
141+
pyperclip.copy(decrypted_secret_value)
142+
print(f'Copied {args.secret_key} for "{args.entity}" into your clipboard.')
129143
print('')
130-
return encrypted_password
144+
return True
131145

132146

133147
def decrypt_pw_file():
@@ -145,4 +159,3 @@ def encrypt_pw_file():
145159

146160
# decrypt_pw_file()
147161
# encrypt_pw_file()
148-

0 commit comments

Comments
 (0)