13
13
import hashlib
14
14
import json
15
15
import random
16
+ import inspect
17
+ import datetime
16
18
17
19
import pyaes
18
20
import pyperclip
21
23
# TODO
22
24
# - Python 3 support
23
25
# - Possibility to protect keys of accounts with a second password
26
+ # - Command to show data of only specific account and key
24
27
# If a key is protected, a second password is required to print value or copy value to clipboard.
28
+ # A key is protected if it contains the attribute 'protected' or 'encrypted' to True
25
29
# - Different cryptographic methods
30
+ # - If defined the environment variable EDITOR_FOR_PASSWORD_MANAGER, then all editions will be done using your that editor and saving intermediate results in temporal documents.
26
31
# - More personalization for the generation of random passwords
27
32
# - Create key with random password
28
33
# - Edit key with random password
34
+ # - Add creation and modification timestamps for accounts and keys of accounts
29
35
# - Consider these libraries:
30
36
# https://github.com/italorossi/ishell
31
37
# https://github.com/jonathanslenders/python-prompt-toolkit
32
38
# Print and ask copy to clipboard
33
- # To use module in pure python for symmetric encription instead of gnugpg
34
39
# Mirar esto:
35
40
# https://stackoverflow.com/questions/42568262/how-to-encrypt-text-with-a-value-in-python/44212550
36
41
# hashlib.sha256("Nobody inspects the spammish repetition").hexdigest()
37
42
38
43
39
44
PASSWORD_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
40
45
41
- WHICH_CMD = 'which'
42
46
CLIPBOARD_ENCODING = 'utf-8'
43
47
44
48
LIST_OF_TRUE_VALUES = ["y" , "yes" , "1" , "on" ]
45
49
LIST_OF_FALSE_VALUES = ["n" , "no" , "0" , "off" ]
46
50
51
+ SIGNATURE = 'Ps54kNc8ZnUqWgxZ'
52
+
47
53
48
54
def get_boolean (c ):
49
55
if isinstance (c , bool ):
@@ -56,9 +62,37 @@ def get_boolean(c):
56
62
return False
57
63
58
64
65
+ def create_timestamp ():
66
+ return datetime .datetime .utcnow ().strftime ("%a %b %d %H:%M:%S %Z %Y" )
67
+
68
+
69
+ def generate_password (password_length ):
70
+ letters = string .ascii_lowercase
71
+ password = '' .join (random .choice (letters ) for i in range (password_length ))
72
+
73
+ return password
74
+
75
+
76
+ def which (program ):
77
+ import os
78
+ def is_exe (fpath ):
79
+ return os .path .isfile (fpath ) and os .access (fpath , os .X_OK )
80
+
81
+ fpath , fname = os .path .split (program )
82
+ if fpath :
83
+ if is_exe (program ):
84
+ return program
85
+ else :
86
+ for path in os .environ ["PATH" ].split (os .pathsep ):
87
+ exe_file = os .path .join (path , program )
88
+ if is_exe (exe_file ):
89
+ return exe_file
90
+
91
+ return None
92
+
93
+
59
94
def executable_exists (name ):
60
- return subprocess .call ([WHICH_CMD , name ],
61
- stdout = subprocess .PIPE , stderr = subprocess .PIPE ) == 0
95
+ return which (name ) is not None
62
96
63
97
64
98
class PasswordManagerException (Exception ):
@@ -72,6 +106,9 @@ def __str__(self):
72
106
return self .__doc__
73
107
74
108
109
+ class BadArguments (PasswordManagerException ):
110
+ """Bad arguments passed to command"""
111
+
75
112
class AccountNotFoundException (PasswordManagerException ):
76
113
"""Account not found"""
77
114
@@ -110,6 +147,7 @@ def info_print(s):
110
147
def echo (text , indent = 0 , indent_char = " " , prefix = "" ):
111
148
print ("\n " .join (prefix + indent_char * indent + l for l in text .splitlines ()))
112
149
150
+
113
151
def decrypt (enc_data , password ):
114
152
key_32 = hashlib .sha256 (password ).digest ()
115
153
@@ -120,7 +158,6 @@ def decrypt(enc_data, password):
120
158
return dec_data
121
159
122
160
123
-
124
161
def encrypt (dec_data , password ):
125
162
key_32 = hashlib .sha256 (password ).digest ()
126
163
@@ -352,6 +389,8 @@ def create_help(command_methods):
352
389
353
390
class Password_Manager (object ):
354
391
__metaclass__ = Password_Manager_Meta
392
+ #PROMPT = "Command:"
393
+ PROMPT = ">"
355
394
356
395
def __init__ (self , filename , master_password = None , title = "Password Manager" ):
357
396
self ._filename = filename
@@ -371,10 +410,26 @@ def all_accounts(self):
371
410
return list_of_accounts
372
411
373
412
def save_accounts (self ):
413
+ modification_date = create_timestamp ()
414
+ self ._root ["last_modification" ] = modification_date
415
+ self ._root ["password_manager" ] = SIGNATURE
416
+
374
417
encrypted_data = encrypt (json .dumps (self ._root ), self ._master_password )
375
418
with open (self ._filename , "wb" ) as f :
376
419
f .write (encrypted_data )
377
420
421
+
422
+ def _print_all_accounts (self ):
423
+ list_of_accounts = self .all_accounts ()
424
+
425
+ if list_of_accounts :
426
+ echo ("List of accounts:" )
427
+ for account_index , account in enumerate (list_of_accounts ):
428
+ echo ("%d. " % account_index + account .name )
429
+
430
+ else :
431
+ warn_print ("No account" )
432
+
378
433
def _ask_hidden_value (self , key_name , old_value = None ):
379
434
while True :
380
435
if old_value is None or old_value == "" :
@@ -525,28 +580,19 @@ def _command_create(self, account_name=None):
525
580
526
581
info_print ("Account added!" )
527
582
528
-
529
583
@cmd ("list" , "l" )
530
584
def _command_list (self ):
531
585
"""List all available account by name"""
586
+ self ._print_all_acounts ()
532
587
533
- list_of_accounts = self .all_accounts ()
534
-
535
- if list_of_accounts :
536
- echo ("List of accounts:" )
537
- for account_index , account in enumerate (list_of_accounts ):
538
- echo ("%d. " % account_index + account .name )
539
-
540
- else :
541
- warn_print ("No account" )
542
588
543
589
@cmd ("delete" , "d" )
544
590
def _command_delete (self , index = None ):
545
591
"""Delete account"""
546
592
547
593
account = self ._ask_account_index (index )
548
594
549
- if self ._ask_confirmation ("are you sure that you want to delete this account?" ):
595
+ if self ._ask_confirmation ("are you sure that you want to delete this account '%s'?" % account . name ):
550
596
account .dump (show_values = self ._show_values )
551
597
552
598
account .remove ()
@@ -725,11 +771,10 @@ def _command_clear(self):
725
771
clear_screen ()
726
772
print (self .HELP_MESSAGE )
727
773
728
- @cmd ("exit" )
774
+ @cmd ("exit" , "q" )
729
775
def _command_exit (self ):
730
776
"""Exit"""
731
777
732
- print "hola"
733
778
clear_screen ()
734
779
exit ()
735
780
@@ -739,8 +784,8 @@ def _command_generate_password(self, password_length=10):
739
784
740
785
password_length = int (password_length )
741
786
742
- letters = string . ascii_lowercase
743
- print '' . join ( random . choice ( letters ) for i in range ( password_length ) )
787
+ password = generate_password ( password_length )
788
+ print ( password )
744
789
745
790
def run (self ):
746
791
if os .path .isfile (self ._filename ):
@@ -771,7 +816,15 @@ def run(self):
771
816
return
772
817
773
818
self ._master_password = master_password
774
- self ._root = {"accounts" : []}
819
+
820
+
821
+ creation_date = create_timestamp ()
822
+ self ._root = {
823
+ "accounts" : [],
824
+ "password_manager" : SIGNATURE ,
825
+ "creation_date" : creation_date ,
826
+ "last_modification" : creation_date
827
+ }
775
828
776
829
self .save_accounts ()
777
830
else :
@@ -782,27 +835,32 @@ def run(self):
782
835
783
836
try :
784
837
root = decrypt (enc_data , self ._master_password )
785
- except DecryptionException as e :
786
- error_print ("Error doing decryption: \n %s" % str ( e ) )
838
+ except ValueError :
839
+ error_print ("Wrong Password" )
787
840
return
788
841
789
- self ._root = json .loads (root )
842
+ try :
843
+ self ._root = json .loads (root )
844
+ except ValueError :
845
+ error_print ("Wrong Password" )
846
+ return
847
+
848
+ if "password_manager" not in self ._root or self ._root ["password_manager" ] != SIGNATURE :
849
+ error_print ("Wrong Password" )
850
+ return
790
851
791
852
clear_screen ()
792
853
793
854
header = "-" * len (self ._title ) + "\n " + self ._title + "\n " + "-" * len (self ._title ) + "\n \n " + self .HELP_MESSAGE
794
855
print (header )
795
856
796
- list_of_accounts = self .all_accounts ()
857
+ self ._print_all_accounts ()
858
+ print ("\n Please enter a command." )
797
859
798
- if list_of_accounts :
799
- print ("List of accounts:" )
800
- for account_index , account in enumerate (list_of_accounts ):
801
- print ("%d. " % account_index + account .name )
802
860
803
861
while True :
804
862
try :
805
- user_input = raw_input ("\n \n Command: " )
863
+ user_input = raw_input ("\n %s " % self . PROMPT )
806
864
except KeyboardInterrupt :
807
865
return
808
866
@@ -823,6 +881,22 @@ def run(self):
823
881
else :
824
882
args .append (t )
825
883
884
+ func_args = inspect .getargspec (command_function ).args [1 :]
885
+
886
+ try :
887
+ if len (args ) > len (func_args ):
888
+ raise BadArguments
889
+
890
+ func_args = func_args [len (args ):]
891
+
892
+ for kw in kwargs :
893
+ if kw not in func_args :
894
+ raise BadArguments
895
+
896
+ except BadArguments :
897
+ error_print ("Bad arguments" )
898
+ continue
899
+
826
900
try :
827
901
command_function (* args , ** kwargs )
828
902
except PasswordManagerException as e :
@@ -832,6 +906,7 @@ def run(self):
832
906
else :
833
907
error_print ("Command not found!" )
834
908
909
+ print ("\n " )
835
910
836
911
if __name__ == "__main__" :
837
912
import argparse
@@ -841,5 +916,4 @@ def run(self):
841
916
args = parser .parse_args ()
842
917
filename = args .filename
843
918
844
-
845
919
Password_Manager (filename ).run ()
0 commit comments