Skip to content

Commit

Permalink
Enhanced and fixed
Browse files Browse the repository at this point in the history
- added shell wildcards (via match and glob functions) expansion, so that most commands can take advantage of it transparently
- more commands accept multiple arguments (notably: encrypt, decrypt)
- modified version of w32lex to add quotes only when really needed
- fixed encryptDir and decryptDir: now they both create also empty directories
- encryptDir now recreates the source directory instead of just copying its contents
  • Loading branch information
maxpat78 committed Oct 18, 2024
1 parent c6f03dc commit f1423ba
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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]
usage: pycryptomator [-h] [--init] [--print-keys [{a85,b64,words}]] [--master-keys PRIMARY_KEY HMAC_KEY]
[--password PASSWORD] [--change-password]
vault_name
Expand Down
3 changes: 2 additions & 1 deletion pycryptomator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
COPYRIGHT = '''Copyright (C)2024, by maxpat78.'''
__version__ = '1.5'
__version__ = '1.6'
__all__ = ["Vault", "init_vault", "backupDirIds"]
from .cryptomator import *
7 changes: 6 additions & 1 deletion pycryptomator/__main__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import locale, sys, argparse, shlex
import locale, sys, argparse
from os.path import *
from .cryptomator import *
from .cmshell import CMShell
from .wordscodec import Wordscodec
if os.name == 'nt':
import pycryptomator.w32lex as shlex # default shlex ban \ in pathnames!
else:
import shlex

"""
MIT License
Expand Down
59 changes: 38 additions & 21 deletions pycryptomator/cmshell.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import cmd, sys, os
import cmd, sys, os, glob
from os.path import *
from .cryptomator import *

if os.name == 'nt':
from .w32lex import split # shlex ban \ in pathnames!
from .w32lex import split, join # shlex ban \ in pathnames!
else:
from shlex import split
from shlex import split, join

class CMShell(cmd.Cmd):
intro = 'PyCryptomator Shell. Type help or ? to list all available commands.'
Expand All @@ -19,8 +19,21 @@ def __init__ (p, vault):
def preloop(p):
p.prompt = '%s:> ' % p.vault.base

def do_debug(p, arg):
pass
def precmd(p, line):
#~ print('debug: cmdline=', line)
# shell wildcards expansion
argl = []
for arg in split(line):
if '?' in arg or '*' in arg:
if argl[0] == 'encrypt':
argl += glob.glob(arg) # probably, we want globbing "real" pathnames
else:
argl += p.vault.glob(arg)
else:
argl += [arg]
line = join(argl)
#~ print('debug: final cmdline=', line)
return line

def do_quit(p, arg):
'Quit the PyCryptomator Shell'
Expand All @@ -32,9 +45,10 @@ def do_alias(p, arg):
if not argl:
print('use: alias <virtual pathname>')
return
i = p.vault.getInfo(argl[0])
print(i.realPathName)

for it in argl:
i = p.vault.getInfo(it)
print(i.realPathName)

def do_backup(p, arg):
'Backup all the dir.c9r with their tree structure in a ZIP archive'
argl = split(arg)
Expand All @@ -50,16 +64,18 @@ def do_decrypt(p, arg):
if move: argl.remove('-m')
force = '-f' in argl
if force: argl.remove('-f')
if not argl or argl[0] == '-h' or len(argl) != 2:
print('use: decrypt [-m] [-f] <virtual_pathname_source> <real_pathname_destination>')
if not argl or argl[0] == '-h' or len(argl) < 2:
print('use: decrypt [-m] [-f] <virtual_pathname_source1...> <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, move)
else:
p.vault.decryptFile(argl[0], argl[1], force, move)
if argl[1] == '-': print()
for it in argl[:-1]:
is_dir = p.vault.getInfo(it).isDir
if is_dir:
p.vault.decryptDir(it, argl[-1], force, move)
else:
p.vault.decryptFile(it, argl[-1], force, move)
if argl[-1] == '-': print()
except:
print(sys.exception())

Expand All @@ -68,14 +84,15 @@ def do_encrypt(p, arg):
argl = split(arg)
move = '-m' in argl
if move: argl.remove('-m')
if not argl or argl[0] == '-h' or len(argl) != 2:
print('use: encrypt [-m] <real_pathname_source> <virtual_pathname_destination>')
if not argl or argl[0] == '-h' or len(argl) < 2:
print('use: encrypt [-m] <real_pathname_source1...> <virtual_pathname_destination>')
return
try:
if isdir(argl[0]):
p.vault.encryptDir(argl[0], argl[1], move=move)
else:
p.vault.encryptFile(argl[0], argl[1], move=move)
for it in argl[:-1]:
if isdir(it):
p.vault.encryptDir(it, argl[-1], move=move)
else:
p.vault.encryptFile(it, argl[-1], move=move)
except:
print(sys.exception())

Expand Down
105 changes: 92 additions & 13 deletions pycryptomator/cryptomator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""
import getpass, hashlib, struct, base64
import json, sys, io, os, operator
import time, zipfile, locale, uuid, shutil
import time, zipfile, locale, uuid, shutil, fnmatch
from os.path import *

try:
Expand Down Expand Up @@ -288,20 +288,24 @@ def encryptFile(p, src, virtualpath, force=False, move=False):
def encryptDir(p, src, virtualpath, force=False, move=False):
if (virtualpath[0] != '/'):
raise BaseException('the vault path must be absolute!')
src_dir = basename(src) # directory name we want to encrypt
real = p.mkdir(virtualpath)
n=0
nn=0
n=0 # files count
nn=0 # dirs count
total_bytes = 0
T0 = time.time()
for root, dirs, files in os.walk(src):
nn+=1
for it in files:
for it in files+dirs:
fn = join(root, it)
dn = join(virtualpath, fn[len(src)+1:]) # target pathname
dn = join(virtualpath, src_dir, fn[len(src)+1:]) # target pathname
p.mkdir(dirname(dn))
if it in files:
total_bytes += p.encryptFile(fn, dn, force, move)
n += 1
else:
p.mkdir(dn) # makes empty directories, also
print(dn)
total_bytes += p.encryptFile(fn, dn, force, move)
n += 1
if move:
print('moved', src)
shutil.rmtree(src)
Expand Down Expand Up @@ -377,15 +381,18 @@ def decryptDir(p, virtualpath, dest, force=False, move=False):
T0 = time.time()
for root, dirs, files in p.walk(virtualpath):
nn+=1
for it in files:
for it in files+dirs:
fn = join(root, it)
dn = join(dest, fn[1:]) # target pathname
bn = dirname(dn) # target base dir
if not exists(bn):
os.makedirs(bn)
if it in files:
total_bytes += p.decryptFile(fn, dn, force, move)
n += 1
else:
if not exists(dn): os.makedirs(dn)
print(dn)
total_bytes += p.decryptFile(fn, dn, force, move)
n += 1
if move:
print('moved', virtualpath)
p.rmtree(virtualpath)
Expand Down Expand Up @@ -535,9 +542,6 @@ def _realsize(n):
return size

info = p.getInfo(virtualpath)
if not info.isDir:
print(virtualpath, 'is not a directory!')
return
if info.pointsTo:
print(virtualpath, 'points to', info.pointsTo)
virtualpath = info.pointsTo
Expand Down Expand Up @@ -635,6 +639,56 @@ def walk(p, virtualpath):
subdir = join(root, it)
yield from p.walk(subdir)

def glob(p, pathname, recursive=True):
"Expand wildcards in pathname returning a list"
#~ print('globbing', pathname)
base, pred = match(pathname)
x = p.getInfo(base)
if not x.exists: return []
if not x.isDir or not pred: return [pathname]

realpath = x.realDir
dirId = x.dirId
root = base
dirs = []
files = []
r = []
for it in os.scandir(realpath):
if it.name == 'dirid.c9r': continue
is_dir = it.is_dir()
if it.name.endswith('.c9s'): # deflated long name
# A c9s dir contains the original encrypted long name (name.c9s) and encrypted contents (contents.c9r)
ename = open(join(realpath, it.name, 'name.c9s')).read()
dname = p.decryptName(dirId.encode(), ename.encode()).decode()
if exists(join(realpath, it.name, 'contents.c9r')): is_dir = False
else:
dname = p.decryptName(dirId.encode(), it.name.encode()).decode()
sl = join(realpath, it.name, 'symlink.c9r')
if is_dir and exists(sl):
# Decrypt and look at symbolic link target
resolved = p.resolveSymlink(join(root, dname), sl)
is_dir = False
if pred:
#~ print('testing %s against %s' % (dname, pred[0]))
if not match(dname, pred[0]):
#~ print('no match')
continue
# intermediate predicate matches directories only
if not is_dir and len(pred) > 1:
#~ print('is file')
continue
if is_dir: dirs += [dname]
else: files += [dname]
pred = pred[1:]
if not pred:
#~ print('predicate exhausted, building result')
for it in dirs+files:
r += [join(root, it)]
return r
for it in dirs:
r += p.glob(join(root, it, *pred), recursive)
return r

# AES utility functions

def aes_unwrap(kek, C):
Expand Down Expand Up @@ -796,3 +850,28 @@ def ask_new_password():
password = getpass.getpass('Please type the new password: ')
check = getpass.getpass('Confirm the password: ')
return password

def match(s, p=None):
"""Test wether a given string 's' matches a predicate 'p' or split the
predicate in two parts, without and with wildcards: origin and predicates list"""
if not s or s == '/': return ('/', [])
aa = s.split('/')
i = 0
if not p:
while i < len(aa):
if '*' in aa[i] or '?' in aa[i]: break
i+=1
#~ print('couple', aa[:i], aa[i:])
first = '/'.join(aa[:i])
second = aa[i:]
if not first: first = '/'
return first, second
bb = p.split('/')
while 1:
if i in (len(aa), len(bb)): break
if not fnmatch.fnmatch(aa[i], bb[i]):
#~ print ('fnmatch',aa,'against',bb,': does not match')
return 0
i+=1
#~ print ('fnmatch',aa,'against',bb,': matches')
return 1
9 changes: 6 additions & 3 deletions pycryptomator/w32lex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
COPYRIGHT = '''Copyright (C)2024, by maxpat78.'''

__all__ = ["split", "quote", "join", "cmd_parse", "cmd_split", "cmd_quote"]
__version__ = '1.0.0'
__version__ = 'pycryptomator'

import os

Expand Down Expand Up @@ -137,7 +136,11 @@ def quote(s):
if backslashes:
# double at end, since we quote hereafter
arg += (2*backslashes)*'\\'
arg = '"'+arg+'"' # always quote argument
# modified to suit CMShell needs
for c in ' \t':
if c in arg:
arg = '"'+arg+'"'
break
return arg

def join(argv):
Expand Down

0 comments on commit f1423ba

Please sign in to comment.