Skip to content

Commit

Permalink
Rewritten CLI help handling and ID store help.
Browse files Browse the repository at this point in the history
  • Loading branch information
foonoxous authored and foonoxous committed Mar 7, 2022
1 parent 55f4d02 commit 4ead68c
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 141 deletions.
154 changes: 17 additions & 137 deletions covert/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,112 +4,9 @@

import colorama

import covert
from covert.cli import main_benchmark, main_dec, main_edit, main_enc, main_id
from covert.clihelp import print_help, print_version

basicusage = """\
Usage:
covert enc [files] [recipients] [signatures] [-A | -o unsuspicious.dat [-a]]
covert dec [-A | unsuspicious.dat] [-i id_ed25519] [-o filesfolder]
covert edit unsuspicious.dat — change text in a passphrase-protected archive
covert id alice:bob [options] — create/manage identity store
"""

shorthdrhelp = f"""\
{basicusage}\
covert help — show full command line help
Running covert enc/dec without arguments asks for a password and a message.
Files and folders get attached together with a message if 'enc -' is specified.
"""

# Short command line help
shortenchelp = """\
-p Passphrase recipient (default)
--wide-open Anyone can open the file (no recipients)
-r PKEY -R FILE Recipient pubkey, .pub file or github:username
-i SKEY Sign with a secret key (string token or id file)
-A Auto copy&paste: ciphertext is copied
-o FILENAME Encrypted file to output (binary unless -a is used)
--pad PERCENT Preferred padding amount (default 5 %)
"""

shortdechelp = """\
-A Auto copy&paste: ciphertext is pasted
-i SKEY Decrypt with secret key (token or file)
-o FILEFOLDER Extract any attached files to
"""

introduction = f"""\
Covert {covert.__version__} - A file and message encryptor with strong anonymity
💣 Things encrypted with this developer preview mayn't be readable evermore
"""

shortcmdhelp = f"""\
{introduction}
{shorthdrhelp}
{shortenchelp}
{shortdechelp}
"""

# Full command line help
hdrhelp = f"""\
{basicusage}\
covert benchmark — run a performance benchmark for decryption and encryption
covert help — show full command line help
Running covert enc/dec without arguments asks for a password and a message.
Files and folders get attached together with a message if 'enc -' is specified.
"""

enchelp = f"""\
Encryption options:
{shortenchelp}\
-a Write base64 encoded output when -o is used
"""

dechelp = f"""\
Decryption options:
{shortdechelp}\
"""

idhelp = """\
ID store management:
-s --secret Show secret keys (by default only shows public keys)
-p --passphrase Change Master ID passphrase
-r PKEY -R FILE Change/set the public key associated with ID local:peer
-i SKEY Change the secret key of the given local ID
-D --delete Delete the ID (local and all its peers, or the given peer)
--delete-entire-idstore Securely erase the entire ID storage
"""

keyformatshelp = """\
Supported key formats:
* age1: To generate a key, run: age-keygen
* ssh-ed25519: To generate a key, run: ssh-keygen -t ed25519
"""

exampleshelp = """\
Examples:
* To encrypt a message using an ssh-ed25519 public key, run:
- covert enc -R ~/.ssh/myfriend.pub -o file
- covert enc -r "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL1hd2CrH/pexUjxNfqhHAaKqGwSmn0+sO/YUXVm9Gt1" -o file
* To decrypt a message using a private ssh-ed25519 key file, run:
- covert dec -i ~/.ssh/id_ed25519 file
"""

cmdhelp = f"""\
{introduction}
{hdrhelp}
{enchelp}
{dechelp}
{idhelp}
{keyformatshelp}
{exampleshelp}
"""

class Args:

Expand Down Expand Up @@ -181,24 +78,22 @@ def __init__(self):
"benchmark": main_benchmark,
}

def print_help(modehelp: str = None):
if modehelp is None:
modehelp = shortcmdhelp
first, rest = modehelp.rstrip().split('\n', 1)
print(f'\x1B[1;44m{first:78}\x1B[0m\n{rest}')
sys.exit(0)

def print_version():
print(shortcmdhelp.split('\n')[0])
sys.exit(0)

def needhelp(av):
"""Check for -h and --help but not past --"""
for a in av:
if a == '--': return False
if a.lower() in ('-h', '--help'): return True
return False

def subcommand(arg):
if arg in ('enc', 'encrypt', '-e'): return 'enc', encargs
if arg in ('dec', 'decrypt', '-d'): return 'dec', decargs
if arg in ('edit'): return 'edit', editargs
if arg in ('id'): return 'id', idargs
if arg in ('bench', 'benchmark'): return 'benchmark', benchargs
if arg in ('help', ): return 'help', {}
return None, {}

def argparse():
# Custom parsing due to argparse module's limitations
av = sys.argv[1:]
Expand All @@ -208,30 +103,18 @@ def argparse():
if any(a.lower() in ('-v', '--version') for a in av):
print_version()

ad = {}
args = Args()
modehelp = None
# Separate mode selector from other arguments
if av[0].startswith("-") and len(av[0]) > 2 and not needhelp(av):
av.insert(1, f'-{av[0][2:]}')
av[0] = av[0][:2]

# Support a few other forms for Age etc. compatibility (but only as the first arg)
if av[0] in ('enc', 'encrypt', '-e'):
args.mode, ad, modehelp = 'enc', encargs, f"{hdrhelp}\n{enchelp}"
elif av[0] in ('dec', 'decrypt', '-d'):
args.mode, ad, modehelp = 'dec', decargs, f"{hdrhelp}\n{dechelp}"
elif av[0] in ('edit', ):
args.mode, ad, modehelp = 'edit', editargs, hdrhelp
elif av[0] in ('id'):
args.mode, ad, modehelp = 'id', idargs, f"{hdrhelp}\n{idhelp}"
elif av[0] in ('bench', 'benchmark'):
args.mode, ad, modehelp = 'benchmark', benchargs, hdrhelp
elif av[0] in ('help', ):
args.mode, ad, modehelp = 'help', {}, cmdhelp
args.mode, ad = subcommand(av[0])

if args.mode == 'help' or needhelp(av):
print_help(modehelp=modehelp)
if args.mode == 'help' and len(av) == 2 and (mode := subcommand(av[1])[0]):
print_help(mode)
print_help(args.mode or "help")

if args.mode is None:
sys.stderr.write(' 💣 Invalid or missing command (enc/dec/edit/id/benchmark/help).\n')
Expand All @@ -256,8 +139,7 @@ def argparse():
if not a.startswith('--') and len(a) > 2:
if any(arg not in shortargs for arg in list(a[1:])):
falseargs = [arg for arg in list(a[1:]) if arg not in shortargs]
sys.stderr.write(f' 💣 {falseargs} is not an argument: covert {args.mode} {a}\n')
sys.exit(1)
print_help(args.mode, f' 💣 Unknown argument: covert {args.mode} {a} (failing -{" -".join(falseargs)})')
a = [f'-{shortarg}' for shortarg in list(a[1:]) if shortarg in shortargs]
if isinstance(a, str):
a = [a]
Expand All @@ -266,8 +148,7 @@ def argparse():
if isinstance(av, int):
continue
if argvar is None:
sys.stderr.write(f'{modehelp}\n 💣 Unknown argument: covert {args.mode} {aprint}\n')
sys.exit(1)
print_help(args.mode, f' 💣 Unknown argument: covert {args.mode} {aprint}')
try:
var = getattr(args, argvar)
if isinstance(var, list):
Expand All @@ -279,8 +160,7 @@ def argparse():
else:
setattr(args, argvar, True)
except StopIteration:
sys.stderr.write(f'{modehelp}\n 💣 Argument parameter missing: covert {args.mode} {aprint}\n')
sys.exit(1)
print_help(args.mode, f' 💣 Argument parameter missing: covert {args.mode} {aprint} …')

return args

Expand Down
169 changes: 169 additions & 0 deletions covert/clihelp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import sys
from typing import NoReturn

import covert

T = "\x1B[1;44m" # titlebar (white on blue)
H = "\x1B[1;37m" # heading (bright white)
C = "\x1B[0;34m" # command (dark blue)
F = "\x1B[1;34m" # flag (light blue)
D = "\x1B[1;30m" # dark / syntax markup
N = "\x1B[0m" # normal color

usage = dict(
enc=f"""\
{C}covert {F}enc --id{N} you:them {D}[{N}{F}-r{N} pubkey {D}|{N} {F}-R{N} pkfile{D}] ⋯{N}
{C}covert {F}enc {D}[{F}-i {N}id.key{D}] [{F}-r {N}pubkey {D}|{N} {F}-R {N}pkfile {D}|{F} -p {D}|{F} --wide-open{D}]… ⋯
⋯ [{F}--pad {N}5{D}] [{F}-A {D}| [{F}-o {N}cipher.dat {D}[{F}-a{D}]] [{N}file.jpg{D}]…{N}
""",
dec=f"{C}covert {F}dec {D}[{F}--id {N}you:them{D}] [{F}-i {N}id.key{D}] [{F}-A {D}|{N} cipher.dat{D}] [{F}-o {N}files/{D}]{N}\n",
id=f"{C}covert {F}id {D}[{N}you:them{D}] [{N}options{D}] —{N} create/manage ID store of your keys\n",
edit=f"{C}covert {F}edit {N}cipher.dat {D}{N} securely keep notes with passphrase protection\n",
bench=f"{C}covert {F}benchmark {D}{N} run a performance benchmark for decryption and encryption\n",
)

usagetext = dict(
enc=f"""\
Encrypt a message and/or files. The first form uses ID store, while the second
does not and instead takes all keys on the command line. When no files are
given or {F}-{N} is included, Covert asks for message input or reads stdin.
{F}--id {N}alice:bob Use ID store for local (alice) and peer (bob) keys
{F}-i {N}seckey Sign the message with your secret key
{F}-r {N}pubkey {F}-R{N} file Encrypt the message for this public key
{F}-p{N} Passphrase encryption (default when no other options)
{F}--wide-open{N} Allow anyone to open the file (no keys or passphrase)
{F}--pad{N} PERCENT Preferred random padding amount (default 5 %)
{F}-o{N} FILENAME Output file (binary ciphertext that looks entirely random)
{F}-a{N} ASCII/text output (default for terminal/clipboard)
{F}-A{N} Auto copy&paste of ciphertext (desktop use)
With ID store, no keys need to be defined on command line, although as a
shortcut one may store a new peer by specifiying a previously unused peer name
and his public key by {C}covert {F}enc --id {N}you:newpeer{F} -r{N} key {D}{N} avoiding the use
of the {F}id{N} subcommand to add the peer public key first. Conversations already
established use forward secret keys and should have no key specified on {F}enc{N}.
Folders may be specified as input files and they will be stored recursively.
Any paths given on command line are stripped off the stored names, such that
each item appears at archive root, avoiding accidental leakage of metadata.
""",
dec=f"""\
Decrypt Covert archive. Tries decryption with options given on command line,
and with all conversations and keys stored in ID store.
{F}--id {N}alice:bob Store the sender as "bob" if not previously known
{F}-i {N}seckey Sign the message with your secret key
{F}-r {N}pubkey {F}-R{N} file Encrypt the message for this public key
{F}-p{N} Passphrase encryption (default when no other options)
{F}--wide-open{N} Allow anyone to open the file (no keys or passphrase)
{F}-o{N} folder Folder where to extract any attached files.
{F}-A{N} Auto copy&paste of ciphertext (desktop use)
""",
edit=f"""\
Avoids having to extract the message in plain text for editing, which could
leave copies on disk unprotected. Use {C}covert {F}enc{N} with a passphrase to create
the initial archive. Attached files and other data are preserved even though
editing overwrites the entire encrypted file.
""",
id=f"""\
The ID store keeps your encryption keys stored securely and enables messaging
with forward secrecy. You only need to enter anyone's public key the first
time you send them a message and afterwards all replies use temporary keys
which change with each message sent and received.
{F}-s --secret{N} Show secret keys (by default only shows public keys)
{F}-p --passphrase{N} Change Master ID passphrase
{F}-r {N}pk {F}-R {N}pkfile Change/set the public key associated with ID local:peer
{F}-i {N}seckey Change the secret key of the given local ID
{F}-D --delete{N} Delete the ID (local and all its peers, or the given peer)
{F}--delete-entire-idstore{N} Securely erase the entire ID storage
The storage is created when you run {C}covert {F}id{N} yourname. Be sure to
write down the master passphrase created or change it to one you can remember.
Multiple local IDs can be created for separating one's different tasks and
their contacts, but all share the same master passphrase.
The ID names are for your own information and are never included in messages.
Avoid using spaces or punctuation on them. Notice that your local ID always
comes first, and any peer is separated by a colon. Deletion of a local ID also
removes all public keys and conversations attached to it.
""",
)

cmdhelp = {k: f"{usage[k]}\n{usagetext.get(k, '')}".rstrip("\n") + "\n" for k in usage}

introduction = f"Covert {covert.__version__} - A file and message encryptor with strong anonymity"
if len(introduction) > 78: # git version string is too long
introduction = f"Covert {covert.__version__} - A file and message encryptor"

introduction = f"""\
{T}{introduction:78}{N}
💣 Things encrypted with this developer preview mayn't be readable evermore
"""

shorthelp = f"""\
{introduction}
{"".join(usage.values())}
Getting started: optionally create an ID ({C}covert {F}id{N} yourname), then use the
{F}enc{N} command to send messages and {F}dec{N} to receive them. You won't need most
of the options but see the help for more info. Commonly used options:
{F}--id {N}alice:bob Use ID store for local (alice) and peer (bob) keys
{F}-i {N}seckey Your secret key file (e.g .ssh/id_ed25519) or keystring
{F}-r {N}pubkey {F}-R{N} file Their public key, or {F}-R{N} github:username {D}|{N} bob.pub
{F}-A{N} Auto copy&paste of ciphertext (desktop use)
{F}--help --version{N} Useful information. Help applies to subcommands too.
"""

keyformatshelp = f"""\
{H}Supported key formats and commands to generate keys:{N}
* Age: {C}covert {F}id{N} yourname (Covert natively uses Age's key format)
* Minisign: {C}minisign {F}-R{N}
* SSH ed25519: {C}ssh-keygen {F}-t ed25519{N} (other SSH key types are not supported)
* WireGuard: {C}wg {F}genkey {C}| tee {N}secret.key {C}| wg {F}pubkey{N}
"""

exampleshelp = f"""\
{H}Examples:{N}
* To encrypt a message using an ssh-ed25519 public key, run:
- {C}covert {F}enc -R {N}github:myfriend {F}-o{N} file
- {C}covert {F}enc -R {N}~/.ssh/myfriend.pub {F}-o{N} file
- {C}covert {F}enc -r {N}AAAAC3NzaC1lZDI1NTE5AAAA... {F}-o{N} file
* To decrypt a message using a private ssh-ed25519 key file, run:
- {C}covert {F}dec -i {N}~/.ssh/id_ed25519 file
* Messaging and key storage with ID store:
- {C}covert {F}id {N}alice Add your ID (generate new or {F}-i{N} key)
- {C}covert {F}id {N}alice:bob {F}-R{N} github:bob Add bob as a peer for local ID alice
- {C}covert {F}enc --id {N}alice:bob Encrypt a message using idstore
- {C}covert {F}enc --id {N}alice:charlie {F}-r{N} pk Adding a peer (on initial message)
- {C}covert {F}dec{N} Uses idstore for decryption
"""

allcommands = '\n\n'.join(cmdhelp.values())

fullhelp = f"""\
{introduction}
{allcommands}
{keyformatshelp}
{exampleshelp}"""

def print_help(modehelp: str = None, error: str = None) -> NoReturn:
stream = sys.stderr if error else sys.stdout
if modehelp is None: stream.write(shorthelp)
elif (h := cmdhelp.get(modehelp)): stream.write(h)
else: stream.write(fullhelp)
if error:
stream.write(f"\n{error}\n")
sys.exit(1)
sys.exit(0)

def print_version() -> NoReturn:
print(f"Covert {covert.__version__}")
sys.exit(0)
Loading

0 comments on commit 4ead68c

Please sign in to comment.