Skip to content

Commit

Permalink
Start of code for #170 - ptrecrypt batch conversion/re-encrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
clach04 committed Nov 15, 2024
1 parent 91c38d2 commit c7cafdb
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 3 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
19 changes: 16 additions & 3 deletions puren_tonbo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?
]

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
128 changes: 128 additions & 0 deletions puren_tonbo/tools/ptrecrypt.py
Original file line number Diff line number Diff line change
@@ -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())
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []),
Expand Down

0 comments on commit c7cafdb

Please sign in to comment.