From 832c82953e2d63f80cc37f4c872435d4cd2dd1e8 Mon Sep 17 00:00:00 2001 From: quadrismegistus Date: Fri, 11 Sep 2020 18:17:21 +0100 Subject: [PATCH] cli work --- komrade/backend/callers.py | 1 - komrade/backend/keymaker.py | 8 +- komrade/backend/people.py | 18 ++- komrade/cli/artcode.py | 79 +++++++++++ komrade/cli/cli.py | 253 ++++++++++++++++++++++++------------ komrade/constants.py | 2 +- komrade/utils.py | 21 ++- 7 files changed, 279 insertions(+), 103 deletions(-) diff --git a/komrade/backend/callers.py b/komrade/backend/callers.py index a97f61e..6f7da60 100644 --- a/komrade/backend/callers.py +++ b/komrade/backend/callers.py @@ -23,4 +23,3 @@ class Caller(Operator): # @hack: repurposing this for now as a narrator -e \ No newline at end of file diff --git a/komrade/backend/keymaker.py b/komrade/backend/keymaker.py index b594afe..bab9c34 100644 --- a/komrade/backend/keymaker.py +++ b/komrade/backend/keymaker.py @@ -117,19 +117,19 @@ class KomradeAsymmetricPrivateKey(KomradeAsymmetricKey): def key(self): return self.privkey def __repr__(self): return f'''[Asymmetric Private Key] ({self.discreet})''' -def make_key_discreet(data,chance_bowdlerize=0.5): +def make_key_discreet(data,chance_redacted=0.5): import random if not data: return '?' if not isBase64(data): data=b64encode(data) key=data.decode() - return ''.join((k if random.random() <| + < <| + < <| + + (2A) (2) + symmetric asymmetric + encryption private + key, from key + hashed + passphrase + +""" + +ART_KEY_PAIR_SPLITTING3 = """ + _ __ _ + /o / o\\ \\ + \ \_ / / + < --(encrypts)--> <| --(into)--> | + < <| | + < <| | + + (2A) (2) (2B) + symmetric asymmetric encrypted form + encryption private of asymmetric + key, from key private key (2) + hashed + passphrase + +""" + @@ -319,6 +372,14 @@ ART_KEY_PAIR3A2 = """ """ +ART_KEY_KEY2A = """ _ + /o + \ + < + < + < + +""" ART_KEY_PAIR3B2 = """ @@ -496,6 +557,24 @@ ART_KEY_PAIR4Z1 = """ form of (2) """ +ART_KEY_PAIR4ZZ = """ + + __ + /o \\_____ + \__/-="="` + + ? _ + ?? \\ + ? _ / + ? | + ? | + ? | + + + +""" + + ART_KEY_PAIR4Z2 = """ __ diff --git a/komrade/cli/cli.py b/komrade/cli/cli.py index a156a7d..d215d05 100644 --- a/komrade/cli/cli.py +++ b/komrade/cli/cli.py @@ -20,8 +20,8 @@ class CLI(Logger): def run(self,inp='',name=''): self.name=name clear_screen() - self.boot() - self.help() + # self.boot() + # self.help() if inp: self.route(inp) @@ -45,7 +45,7 @@ class CLI(Logger): def boot(self,indent=5): logo=art.text2art(CLI_TITLE,font=CLI_FONT) - # logo=make_key_discreet_str(logo,chance_bowdlerize=0.1) #.decode() + # logo=make_key_discreet_str(logo,chance_redacted=0.1) #.decode() logo=tw.indent(logo, ' '*indent) scan_print(logo,max_pause=0.005) @@ -69,7 +69,7 @@ class CLI(Logger): ### DIALOGUES # hello, op? - def status_keymaker_intro(self,name): + def status_keymaker_part1(self,name): self.status(None,{ART_OLDPHONE4+'\n',True},3) #,scan=False,width=None,pause=None,clear=None) nm=name if name else '?' @@ -110,131 +110,216 @@ class CLI(Logger): - def status_keymaker_body(self,name,passphrase,pubkey,privkey,hasher): + def status_keymaker_part2(self,name,passphrase,pubkey,privkey,hasher,persona): + from getpass import getpass # gen what we need uri_id = pubkey.data_b64 qr_str = get_qr_str(uri_id) qr_path = os.path.join(PATH_QRCODES,name+'.png') - # # what are pub/priv? - # self.status( - # 'I will forge for you two matching keys, part of an "asymmetric" pair.', - # 'Please, watch me work.', + # what are pub/priv? + self.status( + 'I will forge for you two matching keys, part of an "asymmetric" pair.', + 'Please, watch me work.', - # None,{tw.indent(ART_KEY,' '*5)+'\n'}, + None,{tw.indent(ART_KEY,' '*5)+'\n'}, - # 'I use a high-level cryptographic function from Themis, a well-respected open-source cryptography library.', - # 'I use the iron-clad Elliptic Curve algorthm to generate the asymmetric keypair.', - # '> GenerateKeyPair(KEY_PAIR_TYPE.EC)', - # 3 - # ) - # self.status( - # None, - # {ART_KEY_PAIR,True} - # ) #,clear=False,indent=10,pause=False) - # self.status( - # None,{ART_KEY_PAIR}, - # 'A matching set of keys have been generated.', - # None,{ART_KEY_PAIR2A+'\n\nA matching set of keys have been generated.'}, - # '1) First, I have made a "public key" which you can share with anyone:', - # f'{repr(pubkey)}', - # 'This key is a randomly-generated binary string, which acts as your "address" on Komrade.', - # 'By sharing this key with someone, you enable them to write you an encrypted message which only you can read.' - # ) - # self.status( - # None,{ART_KEY_PAIR2A}, - # f'You can share your public key by copy/pasting it to them over a secure channel (e.g. Signal).', - # 'Or, you can share it as a QR code, especially phone to phone:', - # {qr_str+'\n\n',True,5}, - # f'\n\n(If registration is successful, this QR code be saved as an image to your device at: {qr_path}.)' - # ) + 'I use a high-level cryptographic function from Themis, a well-respected open-source cryptography library.', + 'I use the iron-clad Elliptic Curve algorthm to generate the asymmetric keypair.', + '> GenerateKeyPair(KEY_PAIR_TYPE.EC)', + 3 + ) + self.status( + None, + {ART_KEY_PAIR,True} + ) #,clear=False,indent=10,pause=False) + self.status( + None,{ART_KEY_PAIR}, + 'A matching set of keys have been generated.', + None,{ART_KEY_PAIR2A+'\n\nA matching set of keys have been generated.'}, + '1) First, I have made a "public key" which you can share with anyone:', + f'{repr(pubkey)}', + 'This key is a randomly-generated binary string, which acts as your "address" on Komrade.', + 'By sharing this key with someone, you enable them to write you an encrypted message which only you can read.' + ) + self.status( + None,{ART_KEY_PAIR2A}, + f'You can share your public key by copy/pasting it to them over a secure channel (e.g. Signal).', + 'Or, you can share it as a QR code, especially phone to phone:', + {qr_str+'\n\n',True,5}, + f'\n\n(If registration is successful, this QR code be saved as an image to your device at: {qr_path}.)' + ) # private keys - # self.status(None, - # {ART_KEY_PAIR2B}, - # 'Second, I have forged a matching "private key":', - # f'{repr(privkey)}', - # 'With it, you can decrypt any message sent to you via your public key.', - # 'You you should never, ever give this key to anyone.', - # 'In fact, this key is so dangerous that I will immediately destroy it by splitting it into two half-keys:' - # ) - # self.status(None, - # {ART_KEY_PAIR31A}, - # {ART_KEY_PAIR3B+'\n',True}, - # 3,'Allow me to explain.', - # '(2A) is a separate encryption key generated by your password.', - # '(2B) is a version of (2) which has been encrypted by (2A).', - # "Because (2) will be destroyed, to rebuild it requires decrypting (2B) with (2A).", - # ) - # self.status( - # None,{ART_KEY_PAIR5+'\n'}, - # "However, in a final move, I will now destroy (2A), too.", - # None,{ART_KEY_PAIR4Z1+'\n'}, - # 'Why? Because now only you can regenerate it, by remembering the password which created it.', - # # None,{ART_KEY_PAIR4Z1+'\n'}, - # 'However, this also means that if you lose or forget your password, you\'re screwed.', - # None,{ART_KEY_PAIR4Z2+'\n'}, - # "Because without key (2A),you couldn never unlock (2B).", - # None,{ART_KEY_PAIR4Z3+'\n'}, - # "And without (2B) and (2A) together, you could never re-assemble the private key of (2).", - # None,{ART_KEY_PAIR4Z42+'\n'}, - # "And without (2), you couldn't read messages sent to your public key.", + self.status(None, + {ART_KEY_PAIR2B}, + 'Second, I have forged a matching "private key":', + f'{repr(privkey)}', + 'With it, you can decrypt any message sent to you via your public key.', + 'You you should never, ever give this key to anyone.', + 'In fact, this key is so dangerous that I will immediately destroy it by splitting it into two half-keys:' + ) + self.status(None, + {ART_KEY_PAIR31A}, + {ART_KEY_PAIR3B+'\n',True}, + 3,'Allow me to explain.', + '(2A) is a separate encryption key generated by your password.', + '(2B) is a version of (2) which has been encrypted by (2A).', + "Because (2) will be destroyed, to rebuild it requires decrypting (2B) with (2A).", + ) + self.status( + None,{ART_KEY_PAIR5+'\n'}, + "However, in a final move, I will now destroy (2A), too.", + None,{ART_KEY_PAIR4Z1+'\n'}, + 'Why? Because now only you can regenerate it, by remembering the password which created it.', + # None,{ART_KEY_PAIR4Z1+'\n'}, + 'However, this also means that if you lose or forget your password, you\'re screwed.', + None,{ART_KEY_PAIR4Z2+'\n'}, + "Because without key (2A),you couldn never unlock (2B).", + None,{ART_KEY_PAIR4Z3+'\n'}, + "And without (2B) and (2A) together, you could never re-assemble the private key of (2).", + None,{ART_KEY_PAIR4Z42+'\n'}, + "And without (2), you couldn't read messages sent to your public key.", ) self.status( - None,{ART_KEY_PAIR4Z1}, + None,{ART_KEY_PAIR4ZZ}, 'So choosing a password is an important thing!' ) if not passphrase: + from getpass import getpass self.status( 'And it looks like you haven\'t yet chosen a password.', 3,"Don't tell it to me! Never tell it to anyone.", "Ideally, don't even save it on your computer; just remember it, or write it down on paper.", "Instead, whisper it to Komrade @Hasher, who scrambles information '1-way', like a blender.", ) + res = self.status(None, - {ART_FROG_BLENDER,True}, + {indent_str(ART_FROG_BLENDER,10),True}, "@Keymaker: Go ahead, try it. Type anything to @Hasher.", ('str_to_hash',f'@{name}: ',input) ) str_to_hash = res.get('vals').get('str_to_hash') - hashed_str = hasher(str_to_hash.encode()) + hashed_str1 = hasher(str_to_hash.encode()) res = self.status( - '@Hasher: '+hashed_str + '@Hasher: '+hashed_str1 ) - res = self.status( - '@Keymaker: See? Ok, now type in a password.' - ('str_to_hash',f'@{name}: ',getpass) + '@Keymaker: Whatever you typed, there\'s no way to reconstruct it from that garbled mess.', + 'But whatever you typed will always produce the *same* garbled mess.', + ('str_to_hash',f'Try typing the exact same thing over again:\n@{name}: ',input) ) str_to_hash = res.get('vals').get('str_to_hash') - hashed_pass1 = hasher(str_to_hash.encode()) + hashed_str2 = hasher(str_to_hash.encode()) res = self.status( - '@Hasher: '+hashed_pass1 + '@Hasher: '+hashed_str2 ) + if hashed_str1==hashed_str2: + self.status('See how the hashed values are also exactly the same?') + else: + self.status('See how the hashed values have also changed?') res = self.status( - '@Keymaker: Whatever you entered, it\'s already forgotten. That hashed mess is all that remains.', - 'Now type in the same password one more time to verify it:', - ('str_to_hash',f'@{name}: ',getpass) + ('str_to_hash',f'Now try typing something just a little bit different:\n@{name}: ',input) ) str_to_hash = res.get('vals').get('str_to_hash') - hashed_pass2 = hasher(str_to_hash.encode()) + hashed_str3 = hasher(str_to_hash.encode()) res = self.status( - '@Hasher: '+hashed_pass2 + '@Hasher: '+hashed_str3 ) - - if hashed_pass1==hashed_pass2: - self.status('The passwords matched.') + if hashed_str2==hashed_str3: + self.status('See how the hashed values are also the same?') else: - self.status('The passwords did not match.') - + self.status('See how the hashed values have also changed?') - + self.status( + None,{indent_str(ART_FROG_BLENDER,10)}, + '@Keymaker: Behind the scenes, @Hasher is using the SHA-256 hashing function, which was designed by the NSA.', + 'But @Hasher also adds a secret "salt" to the recipe, as it\'s called.', + 'To whatever you type in, @Hasher adds a secret phrase: another random string of characters which never changes.', + "By doing so, the hash output is \"salted\": made even more idiosyncratic to outside observers.", + ) + + self.status( + None,{indent_str(ART_FROG_BLENDER,10)}, + f"I've taken the liberty of generating a random secret for your device, which I show here mostly redacted:", + make_key_discreet_str(persona.crypt_keys.secret.decode(),0.25), + 'The full version of this secret is silently added to every input you type into @Hasher.', + "I've saved this secret phrase to a hidden location on your device hardware.", + ) + + self.status( + None,{indent_str(ART_FROG_BLENDER,10)}, + 'However, this means that you will be unable to log in to your account from any other device.', + 'This limitation provides yet another level of hardware protection against network attacks.', + 'However, you can always choose (not recommended) to the secret file with another device by a secure channel.', + 3,f'But, please excuse me Komrade @{name} -- I digress.' + ) + + while not passphrase: + res = self.status(None, + {indent_str(ART_FROG_BLENDER,10)}, + "@Keymaker: Please type your chosen password into @Hasher.", + ('str_to_hash',f'\n@{name}: ',getpass), + pause=False + ) + str_to_hash = res.get('vals').get('str_to_hash') + hashed_pass1 = hasher(str_to_hash.encode()) + res = self.status( + '\n@Hasher: '+hashed_pass1, + pause=False + ) + res = self.status( + '\nNow type in the same password one more time to verify it:', + ('str_to_hash',f'\n@{name}: ',getpass), + pause=False + ) + str_to_hash = res.get('vals').get('str_to_hash') + hashed_pass2 = hasher(str_to_hash.encode()) + res = self.status( + '\n@Hasher: '+hashed_pass2, + pause=False + ) + + if hashed_pass1==hashed_pass2: + self.status('','@Keymaker: Excellent. The passwords clearly matched, because the hashed values matched.',pause=False) + passphrase = hashed_pass1 + else: + self.status('@Keymaker: A pity. It looks like the passwords didn\'t match, since the hashed values didn\'t match either. Try again?') + + return passphrase + + def status_keymaker_part3(self,privkey,privkey_decr,privkey_encr,passphrase): + # self.status( + # None,{tw.indent(ART_KEY,' '*5)+'\n',True}, + # # None,{ART_+'\n',True}, + # 'Now that we have a hashed passphrase, we can generate the (2A) encryption key.', + # {ART_KEY_KEY2A,True,0.1}, + # '''This key (2A) is formed using Themis's high-level symmetric encryption key library, SecureCell using Seal mode.''', + # '''It uses the AES-256 encryption algorithm, which was developed by the U.S. National Institute of Standards and Technology (NIST) in 2001.''' + # ) + s1=self.print('Now that we have (2A), we can use it to encrypt the super-sensitive private key (2):',ret=True) + s2a = self.print(f"(2A) {make_key_discreet_str(passphrase)}",ret=True) + screen1 = f'''{s1}\n\n{ART_KEY_PAIR_SPLITTING1}\n{s2a}''' + + s2 = self.print(f"(2) {make_key_discreet(privkey.data_b64)}",ret=True) + screen2 = f'''{s1}\n\n{ART_KEY_PAIR_SPLITTING2}\n{s2a}\n\n\n{s2}''' + + repr_privkey = repr(privkey).replace('] ',']\n') + s2b = self.print(f"(2B) {make_key_discreet(b64encode(privkey_encr))}",ret=True) + screen3 = f'''{s1}\n\n{ART_KEY_PAIR_SPLITTING3}\n{s2a}\n\n\n{s2}\n\n\n{s2b}''' + self.status(None,s1) + self.status(None,{screen1}) + do_pause() + self.status(None,{screen2}) + do_pause() + self.status(None,{screen3}) + do_pause() diff --git a/komrade/constants.py b/komrade/constants.py index 2a07949..87e06ec 100644 --- a/komrade/constants.py +++ b/komrade/constants.py @@ -188,4 +188,4 @@ PAUSE_LOGGER = 1 CLI_TITLE = 'KOMRADE' CLI_FONT = 'clr5x6'#'colossal' -STATUS_LINE_WIDTH = 50 \ No newline at end of file +CLI_WIDTH = STATUS_LINE_WIDTH = 50 \ No newline at end of file diff --git a/komrade/utils.py b/komrade/utils.py index 07dec53..d53204d 100644 --- a/komrade/utils.py +++ b/komrade/utils.py @@ -71,7 +71,7 @@ class Logger(object): # except KeyboardInterrupt: # exit() - def print(*x,width=STATUS_LINE_WIDTH,end='\n',indent=1,scan=False,**y): + def print(*x,width=STATUS_LINE_WIDTH,end='\n',indent=1,ret=False,scan=False,**y): if not scan and not width: print(*x,end=end,**y) else: @@ -82,6 +82,7 @@ class Logger(object): # xw = [_ for _ in tw.wrap(xs,width=width)] xs=end.join(xw) xs = tw.indent(xs,' '*indent) + if ret: return xs print(xs) if scan==False else scan_print(xs) def status(self,*msg,pause=True,clear=False,ticks=[],tab=2,speed=10,end=None,indent=0,width=80,scan=False): @@ -114,7 +115,7 @@ class Logger(object): elif type(para) is set: # logo/image pl = [x for x in para if type(x)==str] txt=pl[0] - speed =[x for x in para if type(x)==int] + speed =[x for x in para if type(x) in {int,float}] speed = speed[0] if speed else 1 if True in para: scan_print(txt,speed=speed) @@ -260,13 +261,13 @@ def capture_stdout(func): -def scan_print(xstr,min_pause=0,max_pause=0.015,speed=100000): +def scan_print(xstr,min_pause=0,max_pause=.001,speed=1): import random,time for c in xstr: print(c,end='',flush=True) - # naptime=random.uniform(min_pause, max_pause / speed) - # time.sleep(naptime) - time.sleep(.001) + naptime=random.uniform(min_pause, max_pause / speed) + time.sleep(naptime) + # time.sleep() @@ -276,4 +277,10 @@ def get_qr_str(data): qr.add_data(data) ascii = capture_stdout(qr.print_ascii) ascii = ascii[:-1] # removing last line break - return '\n ' + ascii.strip() \ No newline at end of file + return '\n ' + ascii.strip() + + + +def indent_str(x,n): + import textwrap as tw + return tw.indent(x,' '*n) \ No newline at end of file