Skip to content

Commit

Permalink
Re-packaged
Browse files Browse the repository at this point in the history
- code splitted and reorganized
- bug fixed in mkdir
-prepared for publishing with PyPI
  • Loading branch information
maxpat78 committed Oct 15, 2024
1 parent eae9db5 commit 1192d15
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 241 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pycryptomator

A simple Python 3 script to access a Cryptomator V8 vault and carry on some useful operations.
A Python 3 package to access a Cryptomator V8 vault and carry on some useful operations.

```
usage: cryptomator.py [-h] [--init] [--print-keys [{a85,b64,words}]] [--master-keys PRIMARY_KEY HMAC_KEY]
Expand All @@ -25,11 +25,17 @@ options:
--change-password Change the password required to open the vault
```

Passing a couple options, you can show you master keys or recover them in case configuration files are corrupted:
Passing a couple options, you can show you master keys or recover them in case
configuration files are corrupted:

`--print-keys` shows the decrypted primary and hmac master key in ASCII85 or BASE64 form, or as a list of English words like Cryptomator itself, to annotate them in a safe place for recovering purposes.
`--print-keys` shows the decrypted primary and hmac master key in ASCII85
or BASE64 form, or as a list of English words like Cryptomator itself, to
annotate them in a safe place for recovering purposes.

`--master-keys` grants access to the vault even in case of lost configuration files `vault.cryptomator` and/or `masterkey.cryptomator`, provided the master keys as ASCII85 or BASE64 strings; `- -` can be used to read the words list from standard input.
`--master-keys` grants access to the vault even in case of lost configuration
files `vault.cryptomator` and/or `masterkey.cryptomator`, provided the master
keys as ASCII85 or BASE64 strings; `- -` can be used to read the words list
from standard input.


After the `vault_name`, you can specify some useful operations like:
Expand Down
15 changes: 12 additions & 3 deletions mytest.bat
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ rd /s /q mytest >nul
md mytest >nul
SET DNAME="/Nome di directory lungo, anzi, lunghissimo, ossia dal nome veramente lunghissimissimo e tale da venire codificato con oltre 255 caratteri dal codec Base64 di Cryptomator in modo da generare un doppio nome di file cifrato con hash"
SET FNAME="/Nome di file lungo, anzi, lunghissimo, ossia dal nome veramente lunghissimissimo e tale da venire codificato con oltre 255 caratteri dal codec Base64 di Cryptomator in modo da generare un doppio nome di file cifrato con hash.txt"
SET P=cryptomator.py --password=pippo
SET P=py -m pycryptomator --password=pippo
echo ++ Testing vault initialization
%P% --init mytest
echo ++ Testing master keys printing
Expand All @@ -23,9 +23,18 @@ echo ++ Testing decryption to STDOUT
%P% mytest decrypt /link_subdir_mytest.bat -
echo ++ Testing alias
%P% mytest alias /link_subdir_mytest.bat
echo ++ Testing rename
%P% mytest mv /link_subdir_mytest.bat /same_link.bat
echo ++ Testing removing files and directory
%P% mytest rm /link_subdir_mytest.bat
%P% mytest rmdir %DNAME%
%P% mytest rm /same_link.bat
%P% mytest rm %DNAME%
%P% mytest rm %DNAME%/mytest.bat
%P% mytest rmdir %DNAME%
%P% mytest rm %DNAME%
%P% mytest ls
exit /b
%P% mytest encrypt cryptolib-develop /cryptolib-develop
%P% mytest ls
%P% mytest decrypt -f /cryptolib-develop T
%P% mytest backup dirids.zip
%P% mytest rm -f /cryptolib-develop
File renamed without changes.
3 changes: 3 additions & 0 deletions pycryptomator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
COPYRIGHT = '''Copyright (C)2024, by maxpat78.'''
__version__ = '1.0'
__all__ = ["Vault", "init_vault", "backupDirIds"]
87 changes: 87 additions & 0 deletions pycryptomator/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import locale, sys, argparse, shlex
from os.path import *
from .cryptomator import *
from .cmshell import CMShell

"""
MIT License
Copyright (c) 2024 maxpat78
"""

locale.setlocale(locale.LC_ALL, '')

parser = argparse.ArgumentParser(prog='pycryptomator', description="Access to a Cryptomator V8 vault")
parser.add_argument('--init', action="store_true", help="Initialize a new vault in an empty directory")
parser.add_argument('--print-keys', help="Print the raw master keys as a list of English words for Cryptomator (default), in ASCII85 (a85) or BASE64 (b64) format", type=str, choices=['a85','b64','words'], const='words', nargs='?')
parser.add_argument('--master-keys', nargs=2, metavar=('PRIMARY_KEY', 'HMAC_KEY'), help="Primary and HMAC master keys in ASCII85 or BASE64 format, or - - to read a words list from standard input")
parser.add_argument('--password', help="Password to unlock master keys stored in config file")
parser.add_argument('--change-password', help="Change the password required to open the vault", action="store_true")
parser.add_argument('vault_name', help="Location of the existing Cryptomator V8 vault to use")
args, extras = parser.parse_known_args()

if args.init:
init_vault(args.vault_name, args.password)
sys.exit(0)

if not args.password and not args.master_keys:
args.password = getpass.getpass()

if args.master_keys:
if args.master_keys[0] == '-':
words = input('Words list: ')
words = words.split()
if len(words) != 44: raise BaseException('Not enough words')
we = Wordsencoder(join(dirname(sys.argv[0]), '4096words_en.txt'))
b = we.words2bytes(words)
we.validate(b)
pk = b[:32]
hk = b[32:64]
print()
else:
def tryDecode(s):
e = 0
d = b''
try: d = base64.a85decode(s)
except: pass
if len(d) == 32: return d
try: d = base64.urlsafe_b64decode(s)
except: pass
if len(d) == 32: return d
raise BaseException('Could not decode master key "%s"'%s)
pk = tryDecode(args.master_keys[0])
hk = tryDecode(args.master_keys[1])
v = Vault(args.vault_name, pk=pk, hk=hk)
else:
v = Vault(args.vault_name, args.password)

if args.print_keys:
print('\n * * * WARNING !!! * * *\n')
print('KEEP THESE KEYS TOP SECRET!\nFor recovering purposes only.\n')

if args.print_keys == 'a85':
encoder = base64.a85encode
elif args.print_keys == 'b64':
encoder = base64.urlsafe_b64encode
else:
# initialize the words encoder with a dictionary in the same directory
# it contains 4096 English words
we = Wordsencoder(join(dirname(sys.argv[0]), '4096words_en.txt'))
words = we.bytes2words(we.blob(v.pk, v.hk))
print(' '.join(words))
sys.exit(0)
print('Primary master key :', encoder(v.pk).decode())
print('HMAC master key :', encoder(v.hk).decode())
sys.exit(0)

if args.change_password:
v.change_password()
sys.exit(0)

if not extras:
CMShell(v).cmdloop() # start a shell with open vault
else:
# We must re-quote args, shlex should suffice
CMShell(v).onecmd(shlex.join(extras)) # execute single command via shell
141 changes: 141 additions & 0 deletions pycryptomator/cmshell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import cmd, sys, shlex
from os.path import *
from .cryptomator import *

class CMShell(cmd.Cmd):
intro = 'PyCryptomator Shell. Type help or ? to list all available commands.'
prompt = 'PCM:> '
vault = None

def __init__ (p, vault):
p.vault = vault
super(CMShell, p).__init__()

def preloop(p):
p.prompt = '%s:> ' % p.vault.base

def do_debug(p, arg):
pass

def do_quit(p, arg):
'Quit the PyCryptomator Shell'
sys.exit(0)

def do_alias(p, arg):
argl = shlex.split(arg)
if not argl:
print('use: alias <virtual pathname>')
return
i = p.vault.getInfo(argl[0])
print(i.realPathName)

def do_backup(p, arg):
'Backup all the dir.c9r with their tree structure in a ZIP archive'
argl = shlex.split(arg)
if not argl:
print('use: backup <ZIP archive>')
return
backupDirIds(p.vault.base, argl[0])

def do_decrypt(p, arg):
'Decrypt files or directories from the vault'
argl = shlex.split(arg)
force = '-f' in argl
if force: argl.remove('-f')
if not argl or argl[0] == '-h' or len(argl) != 2:
print('use: decrypt [-f] <virtual_pathname_source> <real_pathname_destination>')
print('use: decrypt <virtual_pathname_source> -')
return
try:
is_dir = p.vault.getInfo(argl[0]).isDir
if is_dir: p.vault.decryptDir(argl[0], argl[1], force)
else:
p.vault.decryptFile(argl[0], argl[1], force)
if argl[1] == '-': print()
except:
print(sys.exception())

def do_encrypt(p, arg):
'Encrypt files or directories into the vault'
argl = shlex.split(arg)
if not argl or argl[0] == '-h' or len(argl) != 2:
print('use: encrypt <real_pathname_source> <virtual_pathname_destination>')
return
try:
if isdir(argl[0]):
p.vault.encryptDir(argl[0], argl[1])
else:
p.vault.encryptFile(argl[0], argl[1])
except:
print(sys.exception())

def do_ls(p, arg):
'List files and directories'
argl = shlex.split(arg)
recursive = '-r' in argl
if recursive: argl.remove('-r')
if not argl: argl += ['/'] # implicit argument
if argl[0] == '-h':
print('use: ls [-r] <virtual_path1> [...<virtual_pathN>]')
return
for it in argl:
try:
p.vault.ls(it, recursive)
except:
pass

def do_ln(p, arg):
'Make a symbolic link to a file or directory'
argl = shlex.split(arg)
if len(argl) != 2:
print('use: ln <target_virtual_pathname> <symbolic_link_virtual_pathname>')
return
try:
p.vault.ln(argl[0], argl[1])
except:
print(sys.exception())

def do_mkdir(p, arg):
'Make a directory or directory tree'
argl = shlex.split(arg)
if not argl or argl[0] == '-h':
print('use: mkdir <dir1> [...<dirN>]')
return
for it in argl:
try:
p.vault.mkdir(it)
except:
print(sys.exception())

def do_mv(p, arg):
'Move or rename files or directories'
argl = shlex.split(arg)
if len(argl) < 2 or argl[0] == '-h':
print('please use: mv <source> [<source2>...<sourceN>] <destination>')
return
for it in argl[:-1]:
p.vault.mv(it, argl[-1])

def do_rm(p, arg):
'Remove files and directories'
argl = shlex.split(arg)
force = '-f' in argl
if force: argl.remove('-f')
if not argl or argl[0] == '-h':
print('use: rm <file1|dir1> [...<fileN|dirN>]')
return
for it in argl:
if it == '/':
print("Won't erase root directory.")
return
try:
i = p.vault.getInfo(it)
if not i.isDir:
p.vault.remove(it) # del file
continue
if force:
p.vault.rmtree(it) # del dir, even if nonempty
continue
p.vault.rmdir(it) # del empty dir
except:
print(sys.exception())
Loading

0 comments on commit 1192d15

Please sign in to comment.