diff --git a/README.md b/README.md index 93cea59..9bb4f4a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Purēntonbo * `ptcat` - in addition to command line and environment variables, also has an (optional) config file and the concept of a root directory of notes * `ptgrep` - a grep, [ack](https://beyondgrep.com/), [ripgrep](https://github.com/BurntSushi/ripgrep), [silver-searcher](https://geoff.greer.fm/ag/), [pss](https://github.com/eliben/pss) like tool that works on encrypted (and plain text) files * `ptig` an interactive grep like tool that can also view/edit + * `ptrecrypt` a TODO * `ptpyvim` a vim-like editor that works on encrypted (and plain text) files * `ptdiff3merge` 3-way diff/merge too that can works with encrypted (and plain text) files @@ -269,6 +270,10 @@ find filenames ONLY encrypted with regex python -m puren_tonbo.tools.ptgrep --note-root=puren_tonbo/tests/data -y -k -r ^aesop +### ptrecrypt + +TODO + ### ptig Command line interactive search tool, that also supports viewing and editing. diff --git a/puren_tonbo/__init__.py b/puren_tonbo/__init__.py index cc15e00..c49bf33 100644 --- a/puren_tonbo/__init__.py +++ b/puren_tonbo/__init__.py @@ -196,10 +196,19 @@ def __str__(self): class BaseFile: description = 'Base Encrypted File' - extensions = [] # non-empty list of file extensions, first is the default (e.g. for writing) + extensions = [] # non-empty list of file extensions, first is the default (e.g. for writing) and last should be the most generic kdf = None # OPTIONAL key derivation function, that takes a single parameter of bytes for the password/key. See TomboBlowfish # TODO review this needs_key = True # if not true, then this class does not require a key (password) to operate + def default_extension(self): + return self.extensions[0] # pick the first one + + def split_extension(self, filename): + for extn in self.extensions: + if filename.endswith(extn): + return filename[:-len(extn)], extn + pass + def __init__(self, key=None, password=None, password_encoding='utf8'): """ key - is the actual encryption key in bytes @@ -542,6 +551,8 @@ def write_to(self, file_object, byte_data): class Jenc(EncryptedFile): description = 'Markor / jpencconverter pbkdf2-hmac-sha512 iterations 10000 AES-256-GCM' extensions = [ + # TODO u001 + '.v100.jenc', # md and txt? '.jenc', # md and txt? ] @@ -780,6 +791,7 @@ def filename2handler(filename, default_handler=None): elif filename.endswith('.oldstored.zip'): file_extn = '.oldstored.zip' else: + # TODO loop through extensions in class _dummy, file_extn = os.path.splitext(filename) log.debug('clach04 DEBUG file_extn: %r', file_extn) log.debug('clach04 DEBUG file_type_handlers: %r', file_type_handlers) @@ -1338,7 +1350,7 @@ def note_contents_save_native_filename(note_text, filename=None, original_filena validate_filename_generator(filename_generator) filename_generator_func = filename_generators[filename_generator] log.debug('filename_generator_func %r', filename_generator_func) - file_extension = handler.extensions[0] # pick the first one + file_extension = handler.extensions[0] # pick the first one - TODO refactor into a function/method - call handler.default_extension() filename_without_path_and_extension = filename_generator_func(note_text) filename = os.path.join(folder, filename_without_path_and_extension + file_extension) @@ -2219,7 +2231,7 @@ def note_contents_save(self, note_text, filename=None, original_filename=None, f if filename is None: if handler_class is None: raise NotImplementedError('Missing handler_class for missing filename, could default to Raw - make decision') - file_extension = handler_class.extensions[0] # pick the first one + file_extension = handler_class.extensions[0] # pick the first one - TODO refactor into a function/method - call handler_class.default_extension() - Is this callable? Is there a test suite for this code path? if folder: native_folder = self.native_full_path(folder) else: @@ -2403,6 +2415,7 @@ def pt_open(file, mode='r', encoding=None): """ filename = file if mode not in ['r', 'w']: + # TODO binary mode raise NotImplemented('mode %r' % mode) handler_class = filename2handler(filename, default_handler=RawFile) if not encoding: diff --git a/puren_tonbo/tools/ptrecrypt.py b/puren_tonbo/tools/ptrecrypt.py new file mode 100644 index 0000000..c95dd0f --- /dev/null +++ b/puren_tonbo/tools/ptrecrypt.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# -*- coding: us-ascii -*- +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +"""Command line tool to re-encrypt Puren Tonbo files from any format into any format, optionally with a new password + + python -m puren_tonbo.tools.ptrecrypt -h + ptrecrypt -h + + python -m puren_tonbo.tools.ptrecrypt puren_tonbo/tests/data +""" + +import datetime +import glob +import os +from optparse import OptionParser +import sys +import tempfile + +import puren_tonbo +import puren_tonbo.ui + + +is_py3 = sys.version_info >= (3,) + +def main(argv=None): + if argv is None: + argv = sys.argv + + usage = "usage: %prog [options] file_or_dir_pattern1 [file_or_dir_pattern2...]" + parser = OptionParser(usage=usage, version="%%prog %s" % puren_tonbo.__version__) + parser.add_option("--list-formats", help="Which encryption/file formats are available", action="store_true") + parser.add_option("--password-prompt", "--password_prompt", help="Comma seperated list of prompt mechanism to use, options; " + ','.join(puren_tonbo.ui.supported_password_prompt_mechanisms()), default="any") + parser.add_option("--no-prompt", "--no_prompt", help="do not prompt for password", action="store_true") + parser.add_option("--cipher", help="Which encryption mechanism to use (file extension used as hint), use existing cipher if ommited") + parser.add_option("--new-password", "--new_password", help="new password to use, if omitted use the existing password") + parser.add_option("-E", "--envvar", help="Name of environment variable to get password from (defaults to PT_PASSWORD) - unsafe", default="PT_PASSWORD") # similar to https://ccrypt.sourceforge.net/ + parser.add_option("-p", "--password", help="password, if omitted but OS env PT_PASSWORD is set use that, if missing prompt") + parser.add_option("-P", "--password_file", help="file name where password is to be read from, trailing blanks are ignored") + parser.add_option("-v", "--verbose", action="store_true") + parser.add_option("-s", "--silent", help="if specified do not warn about stdin using", action="store_false", default=True) + # TODO option on resolving files that already exist; default error/stop, skip, overwrite (in safe mode - needed for same file type, new password) + # TODO option on saving to delete original file + (options, args) = parser.parse_args(argv[1:]) + print('%r' % ((options, args),)) + verbose = options.verbose + if verbose: + print('Python %s on %s' % (sys.version.replace('\n', ' - '), sys.platform)) + if options.list_formats: + puren_tonbo.print_version_info() + return 0 + + def usage(): + parser.print_usage() + + if not args: + parser.print_usage() + + if options.cipher: + handler_class_newfile = puren_tonbo.filename2handler('_.' + options.cipher) # TODO options.cipher to filename extension is less than ideal + else: + handler_class_newfile = None + + if options.no_prompt: + options.password_prompt = None + default_password_value = '' # empty password, cause a bad password error + else: + options.password_prompt = options.password_prompt.split(',') # TODO validation options? for now rely on puren_tonbo.getpassfunc() + default_password_value = None + + if options.password_file: + f = open(options.password_file, 'rb') + password_file = f.read() + f.close() + password_file = password_file.strip() + else: + password_file = None + + password = options.password or password_file or os.environ.get(options.envvar or 'PT_PASSWORD') or puren_tonbo.keyring_get_password() or default_password_value + if password is None: + # get password ahead of file reading + # TODO review wrong password behavior, should prompt. + password = puren_tonbo.ui.getpassfunc("Puren Tonbo ptcipher Password:", preference_list=options.password_prompt) + if password and not isinstance(password, bytes): + password = password.encode('us-ascii') + new_password = options.new_password or password # TODO document/make clear - if password changes during processing, first password passed in is what will be used for new password + + filename_pattern_list = args + directory_list = [] + print('%r' % ((argv, args, directory_list),)) + #import pdb; pdb.set_trace() + for filename_pattern in filename_pattern_list: + # NOTE local file system only + # + if os.path.isdir(filename_pattern): + directory_list.append(filename_pattern) + continue + for filename in glob.glob(filename_pattern): + print('TODO Process %s' % filename) + filename_abs = os.path.abspath(filename_pattern) + print('\t %s' % filename_abs) + # determine filename sans extension. See new note code in ptig? function to add to handler class? COnsider implemention here then refactor later to place in to pt lib + #note_contents_load_filename(filename_abs, get_pass=None, dos_newlines=False, return_bytes=True, handler_class=None, note_encoding='utf-8') + # TODO caching password... + #plaintext_bytes = puren_tonbo.note_contents_load_filename(filename_abs, get_pass=password, dos_newlines=False, return_bytes=True) # get raw bytes, do not treat like a notes (text) file + # alternatively (future?) call pt_open() instead + #"""# final alt: + in_handler_class = puren_tonbo.filename2handler(filename_abs) + in_handler = in_handler_class(key=password) + in_file = open(filename_abs, 'rb') + plaintext_bytes = in_handler.read_from(in_file) + in_file.close() + base_filename, original_extension = in_handler.split_extension(filename_abs) + #""" + print('\t\t %r' % plaintext_bytes) + out_handler_class = handler_class_newfile or in_handler_class + out_handler = out_handler_class(new_password) + print('\t\t %r' % ((base_filename, original_extension, original_extension in out_handler.extensions, out_handler.default_extension()),)) + # TODO derive new filename (which may either be new, or replace old/existing for password-change-only operation) + #new_filename_abs = process(filename_abs) + + if directory_list: + raise NotImplementedError('dir support, %r not handled' % directory_list) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py index 406b4ef..8ccff74 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ 'ptcipher = puren_tonbo.tools.ptcipher:main', 'ptconfig = puren_tonbo.tools.ptconfig:main', 'ptgrep = puren_tonbo.tools.ptgrep:main', + 'ptrecrypt = puren_tonbo.tools.ptrecrypt:main', 'ptig = puren_tonbo.tools.ptig:main', 'pttkview = puren_tonbo.tools.pttkview:main', # Assume tk available ] + (['ptpyvim = puren_tonbo.tools.ptpyvim:main'] if pyvim else []),