Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ID store in a file #81

Merged
merged 36 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
16bdbb4
Initial implementation of idstore file where keys can be stored durin…
Feb 3, 2022
26672a5
Cleanup memoryview and other handles after decryption, avoids mmap cl…
Feb 12, 2022
94b657b
Disable Bandit warning on import subprocess, which is useless because…
Feb 12, 2022
8cb1399
Add a contextmanager for printing and clearing console messages.
Feb 13, 2022
7981e40
ID store implemented for covert dec, plenty of cleanup. Use xdg confi…
Feb 13, 2022
69fd584
Use tty.status for cleaner code.
Feb 13, 2022
8ec1cf7
idfilename is now a variable, not a function
Feb 13, 2022
d6c6e1a
Do not use user HOME dir for tests.
Feb 13, 2022
0de0b8c
Fix config dir name
Feb 13, 2022
eb2c396
Make signature block encryption use X25519 keys, avoiding any depende…
Feb 13, 2022
abfe0d4
Fix tests for the changed signatures.
Feb 13, 2022
11ad5bb
Allow using local IDs as peers. Display unlocking key/id on covert dec.
Feb 13, 2022
fdf098c
Hack to show ID names on signatures.
Feb 13, 2022
0a80d67
Updated signature messages
Feb 13, 2022
3c7bdb5
Refactor path and configdir handling to covert.path.
Feb 13, 2022
8b49f51
covert id subcommand implemented.
Feb 14, 2022
91a55dd
Fix changed signature message in tests.
Feb 14, 2022
64ac296
Use XDG datadir instead of config dir for idstore.
Feb 19, 2022
bbc67d5
Draft implementation of Signal Double Ratchet for PFS. Not functional…
Feb 20, 2022
c9c2237
Ratchet working in principle.
Feb 22, 2022
ba93ede
Merge branch 'main' into idstore
Feb 22, 2022
a80d8cd
Fixes and additional tests to ratchet code.
Feb 23, 2022
b0416dc
100 % coverage on ratchet.
Feb 23, 2022
2c5dd4a
Some tests for idstore management commands.
Feb 23, 2022
1e2056c
ID store enc/dec tests.
Feb 23, 2022
9154e0d
Double Ratchet with public key handshakes to forward secrecy. Operati…
Feb 28, 2022
96adedb
Change variable name.
Mar 5, 2022
11a49c7
Update specs with general ratchet operation.
Mar 5, 2022
c6246d1
Setting of keys and fixes to covert id subcommand.
Mar 5, 2022
d10c5fe
Ratchet info on id command.
Mar 5, 2022
cf66baf
Fix a typo in ratchet store. Print current conversation id in CLI dec…
Mar 5, 2022
ae4d519
Ratchet end-to-end tests.
Mar 5, 2022
c8fc51e
Increased test coverage, minor fixes.
Mar 5, 2022
b66ac66
Remove example idstore structure from source.
Mar 5, 2022
55f4d02
Addn test
Mar 5, 2022
4ead68c
Rewritten CLI help handling and ID store help.
Mar 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 38 additions & 124 deletions covert/__main__.py
Original file line number Diff line number Diff line change
@@ -1,106 +1,18 @@
import os
import sys
from typing import NoReturn
import colorama
import covert
from covert.cli import main_benchmark, main_dec, main_edit, main_enc

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
"""

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 help — show full command line help
covert benchmark — run a performance benchmark for decryption and encryption

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}\
"""

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
import colorama

* To decrypt a message using a private ssh-ed25519 key file, run:
- covert dec -i ~/.ssh/id_ed25519 file
"""
from covert.cli import main_benchmark, main_dec, main_edit, main_enc, main_id
from covert.clihelp import print_help, print_version

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

class Args:

def __init__(self):
self.mode = None
self.idname = ""
self.files = []
self.wideopen = None
self.askpass = 0
Expand All @@ -113,9 +25,13 @@ def __init__(self):
self.armor = None
self.paste = None
self.debug = None
self.delete_entire_idstore = False
self.delete = False
self.secret = False


encargs = dict(
idname='-I --id'.split(),
askpass='-p --passphrase'.split(),
passwords='--password'.split(),
wideopen='--wide-open'.split(),
Expand All @@ -130,6 +46,7 @@ def __init__(self):
)

decargs = dict(
idname='-I --id'.split(),
askpass='-p --passphrase'.split(),
passwords='--password'.split(),
identities='-i --identity'.split(),
Expand All @@ -138,6 +55,17 @@ def __init__(self):
debug='--debug'.split(),
)

idargs = dict(
askpass='-p --passphrase'.split(),
recipients='-r --recipient'.split(),
recipfiles='-R --keyfile --recipients-file'.split(),
identities='-i --identity'.split(),
secret='-s --secret'.split(),
delete_entire_idstore='--delete-entire-idstore'.split(),
delete='-D --delete'.split(),
debug='--debug'.split(),
)

editargs = dict(debug='--debug'.split(),)
benchargs = dict(debug='--debug'.split(),)

Expand All @@ -146,27 +74,26 @@ def __init__(self):
"enc": main_enc,
"dec": main_dec,
"edit": main_edit,
"id": main_id,
"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 @@ -176,31 +103,21 @@ 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 ('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/benchmark/help).\n')
sys.stderr.write(' 💣 Invalid or missing command (enc/dec/edit/id/benchmark/help).\n')
sys.exit(1)

aiter = iter(av[1:])
Expand All @@ -222,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 @@ -232,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 @@ -245,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
42 changes: 29 additions & 13 deletions covert/blockstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import mmap
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
from contextlib import contextmanager, suppress
from hashlib import sha512
from secrets import token_bytes

from nacl.exceptions import CryptoError

from covert import chacha, pubkey
from covert import chacha, pubkey, ratchet
from covert.cryptoheader import Header, encrypt_header
from covert.elliptic import xed_sign, xed_verify
from covert.util import noncegen
Expand All @@ -17,14 +17,16 @@

def decrypt_file(auth, f, archive):
b = BlockStream()
b.decrypt_init(f)
if not b.header.key:
for a in auth:
with suppress(CryptoError):
b.authenticate(a)
break
yield from b.decrypt_blocks()
b.verify_signatures(archive)
with b.decrypt_init(f):
if not b.header.key:
for a in auth:
with suppress(CryptoError):
b.authenticate(a)
break
# In case auth is a generator, close it immediately (otherwise would be delayed)
if hasattr(auth, "close"): auth.close()
yield from b.decrypt_blocks()
b.verify_signatures(archive)

class BlockStream:
def __init__(self):
Expand All @@ -33,17 +35,23 @@ def __init__(self):
self.workers = 8
self.executor = ThreadPoolExecutor(max_workers=self.workers)
self.blkhash = None
self.file = None
self.ciphertext = None
self.q = collections.deque()
self.pos = 0 # Current position within self.ciphertext; queued for decryption, not decoded
self.end = 0


def authenticate(self, anykey):
"""Attempt decryption using secret key or password hash"""
if isinstance(anykey, pubkey.Key):
if isinstance(anykey, ratchet.Ratchet):
self.header.try_ratchet(anykey)
elif isinstance(anykey, pubkey.Key):
self.header.try_key(anykey)
else:
self.header.try_pass(anykey)

@contextmanager
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, decrypt.py:DecryptView class should call decrypt_init() inside a with statement.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no decrypt.py or DecryptView class, and decrypt_init() is called within two with statements in blockstream.py and cli.py. Also, this does not seem relevant to this PR.

def decrypt_init(self, f):
self.pos = 0
if hasattr(f, "__len__"):
Expand All @@ -58,6 +66,14 @@ def decrypt_init(self, f):
self.end = 0
size = self._read(1024)
self.header = Header(self.ciphertext[:size])
try:
yield
finally:
self.ciphertext.release()
self.ciphertext = None
self.file = None
self.pos = self.end = 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more clear to zero these variables on separate lines.



def _add_to_queue(self, p, extlen, aad=None):
pos, end = p, p + extlen
Expand Down Expand Up @@ -140,7 +156,7 @@ def verify_signatures(self, a):
a.signatures = []
# Signature verification
if a.index.get('s'):
signatures = [pubkey.Key(edpk=k) for k in a.index['s']]
signatures = [pubkey.Key(pk=k) for k in a.index['s']]
for key in signatures:
sz = self._read(self.end - self.pos + 80)
if sz < 80:
Expand All @@ -156,7 +172,7 @@ def verify_signatures(self, a):
continue
try:
xed_verify(key.pk, self.blkhash, signature)
a.signatures.append((True, key, 'Signature verified'))
a.signatures.append((True, key, 'Signed by'))
except CryptoError:
a.signatures.append((False, key, 'Forged signature'))

Expand Down
Loading