Skip to content

Commit a7bb0fa

Browse files
committed
Working on python API and tests
Tests verify all param combinations wip
1 parent c37642e commit a7bb0fa

File tree

3 files changed

+388
-141
lines changed

3 files changed

+388
-141
lines changed

example/end_to_end_example.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ mkdir -p "$TMP_DIR"
88
rm -rf "$TMP_DIR"
99

1010

11-
1211
# Make a git repo and add some public content
1312
DEMO_REPO=$TMP_DIR/repo
1413
mkdir -p "$DEMO_REPO"

tests/test_transcrypt.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"""
2+
Requirements:
3+
pip install gpg_lite
4+
pip install ubelt
5+
"""
6+
import ubelt as ub
7+
import os
8+
9+
SALTED = 'U2FsdGV'
10+
11+
12+
class TranscryptAPI:
13+
default_config = {
14+
'cipher': 'aes-256-cbc',
15+
'password': 'correct horse battery staple',
16+
'digest': 'md5',
17+
'use_pbkdf2': '0',
18+
'salt_method': 'password',
19+
'config_salt': '',
20+
}
21+
22+
def __init__(self, dpath, config=None, verbose=2, transcript_exe=None):
23+
self.dpath = dpath
24+
self.verbose = verbose
25+
self.transcript_exe = ub.Path(ub.find_exe('transcrypt'))
26+
self.env = {}
27+
self.config = self.default_config.copy()
28+
if config:
29+
self.config.update(config)
30+
31+
def cmd(self, command, shell=False):
32+
return ub.cmd(command, cwd=self.dpath, verbose=self.verbose,
33+
env=self.env, shell=shell)
34+
35+
def login(self):
36+
command = (
37+
"{transcript_exe} -c '{cipher}' -p '{password}' "
38+
"-md '{digest}' --use-pbkdf2 '{use_pbkdf2}' "
39+
"-sm '{salt_method}' "
40+
"-cs '{config_salt}' "
41+
"-y"
42+
).format(transcript_exe=self.transcript_exe, **self.config)
43+
self.cmd(command)
44+
45+
def logout(self):
46+
self.cmd(f'{self.transcript_exe} -f -y')
47+
48+
def display(self):
49+
self.cmd(f'{self.transcript_exe} -d')
50+
51+
def export_gpg(self, recipient):
52+
self.cmd(f'{self.transcript_exe} --export-gpg "{recipient}"')
53+
self.crypt_dpath = self.cmd('git config --local transcrypt.crypt-dir')['out'] or self.dpath / '.git/crypt'
54+
asc_fpath = (self.crypt_dpath / (recipient + '.asc'))
55+
return asc_fpath
56+
57+
def import_gpg(self, asc_fpath):
58+
command = f"{self.transcript_exe} --import-gpg '{asc_fpath}' -y"
59+
self.cmd(command)
60+
61+
def show_raw(self, fpath):
62+
return self.cmd(f'{self.transcript_exe} -s {fpath}')['out']
63+
64+
def _manual_hack_info(self):
65+
"""
66+
Info on how to get an env to run a failing command manually
67+
"""
68+
for k, v in self.env.items():
69+
print(f'export {k}={v}')
70+
print(f'cd {self.dpath}')
71+
72+
73+
class TestEnvironment:
74+
75+
def __init__(self, dpath=None, config=None, verbose=2):
76+
if dpath is None:
77+
# import tempfile
78+
# self._tmpdir = tempfile.TemporaryDirectory()
79+
# dpath = self._tmpdir.name
80+
dpath = ub.Path.appdir('transcrypt/tests/test_env')
81+
self.dpath = ub.Path(dpath)
82+
self.gpg_store = None
83+
self.repo_dpath = None
84+
self.verbose = verbose
85+
self.tc = None
86+
self.config = config
87+
88+
def setup(self):
89+
self._setup_gpg()
90+
self._setup_git()
91+
self._setup_transcrypt()
92+
return self
93+
94+
def _setup_gpg(self):
95+
import gpg_lite
96+
self.gpg_home = (self.dpath / 'gpg').ensuredir()
97+
self.gpg_store = gpg_lite.GPGStore(
98+
gnupg_home_dir=self.gpg_home
99+
)
100+
self.gpg_fpr = self.gpg_store.gen_key(
101+
full_name='Emmy Noether',
102+
email='emmy.noether@uni-goettingen.de',
103+
passphrase=None,
104+
key_type='eddsa',
105+
subkey_type='ecdh',
106+
key_curve='Ed25519',
107+
subkey_curve='Curve25519'
108+
)
109+
# Fix GNUPG permissions
110+
(self.gpg_home / 'private-keys-v1.d').ensuredir()
111+
# 600 for files and 700 for directories
112+
ub.cmd('find ' + str(self.gpg_home) + r' -type f -exec chmod 600 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home)
113+
ub.cmd('find ' + str(self.gpg_home) + r' -type d -exec chmod 700 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home)
114+
115+
def _setup_git(self):
116+
import git
117+
# Make a git repo and add some public content
118+
repo_name = 'demo-repo'
119+
self.repo_dpath = (self.dpath / repo_name).ensuredir()
120+
# self.repo_dpath.delete().ensuredir()
121+
self.repo_dpath.ensuredir()
122+
123+
for content in self.repo_dpath.iterdir():
124+
content.delete()
125+
126+
self.git = git.Git(self.repo_dpath)
127+
self.git.init()
128+
readme_fpath = (self.repo_dpath / 'README.md')
129+
readme_fpath.write_text('content')
130+
self.git.add(readme_fpath)
131+
132+
# Create safe directory that we will encrypt
133+
gitattr_fpath = self.repo_dpath / '.gitattributes'
134+
gitattr_fpath.write_text(ub.codeblock(
135+
'''
136+
safe/* filter=crypt diff=crypt merge=crypt
137+
'''))
138+
self.git.add(gitattr_fpath)
139+
self.git.commit('-am Add initial contents')
140+
self.safe_dpath = (self.repo_dpath / 'safe').ensuredir()
141+
self.secret_fpath = self.safe_dpath / 'secret.txt'
142+
143+
def _setup_transcrypt(self):
144+
self.tc = TranscryptAPI(self.repo_dpath, self.config,
145+
verbose=self.verbose)
146+
err = self.tc.cmd(f'{self.tc.transcript_exe} -d')['err'].strip()
147+
if err != 'transcrypt: the current repository is not configured':
148+
raise AssertionError(f"Got {err}")
149+
self.tc.login()
150+
self.secret_fpath.write_text('secret content')
151+
self.git.add(self.secret_fpath)
152+
self.git.commit('-am add secret')
153+
self.tc.display()
154+
if self.gpg_home is not None:
155+
self.tc.env['GNUPGHOME'] = str(self.gpg_home)
156+
157+
def test_round_trip(self):
158+
ciphertext = self.tc.show_raw(self.secret_fpath)
159+
plaintext = self.secret_fpath.read_text()
160+
assert ciphertext.startswith(SALTED)
161+
assert plaintext.startswith('secret content')
162+
assert not plaintext.startswith(SALTED)
163+
164+
self.tc.logout()
165+
logged_out_text = self.secret_fpath.read_text()
166+
assert logged_out_text == ciphertext
167+
168+
self.tc.login()
169+
logged_in_text = self.secret_fpath.read_text()
170+
171+
assert logged_out_text == ciphertext
172+
assert logged_in_text == plaintext
173+
174+
def test_export_gpg(self):
175+
self.tc.display()
176+
asc_fpath = self.tc.export_gpg(self.gpg_fpr)
177+
178+
info = self.tc.cmd(f'gpg --batch --quiet --decrypt "{asc_fpath}"')
179+
content = info['out']
180+
181+
got_config = dict([p.split('=', 1) for p in content.split('\n') if p])
182+
config = self.tc.config.copy()
183+
is_ok = got_config == config
184+
if not is_ok:
185+
if config['salt_method'] == 'configured':
186+
if config['config_salt'] == '':
187+
config.pop('config_salt')
188+
got_config.pop('config_salt')
189+
is_ok = got_config == config
190+
else:
191+
config.pop('config_salt')
192+
got_config.pop('config_salt')
193+
is_ok = got_config == config
194+
195+
if not is_ok:
196+
print(f'got_config={got_config}')
197+
print(f'config={config}')
198+
raise AssertionError
199+
200+
# content = io.StringIO()
201+
# with open(asc_fpath, 'r') as file:
202+
# ciphertext = file.read()
203+
# self.gpg_store.decrypt(ciphertext, content)
204+
205+
assert asc_fpath.exists()
206+
self.tc.logout()
207+
self.tc.import_gpg(asc_fpath)
208+
209+
plaintext = self.secret_fpath.read_text()
210+
assert plaintext.startswith('secret content')
211+
212+
def test_rekey(self):
213+
# TODO
214+
pass
215+
216+
217+
def run_tests():
218+
"""
219+
CommandLine:
220+
xdoctest -m /home/joncrall/code/transcrypt/tests/test_transcrypt.py run_tests
221+
222+
Example:
223+
>>> import sys, ubelt
224+
>>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests'))
225+
>>> from test_transcrypt import * # NOQA
226+
>>> self = TestEnvironment()
227+
>>> self.setup()
228+
>>> self.tc._manual_hack_info()
229+
>>> self.test_round_trip()
230+
>>> self.test_export_gpg()
231+
232+
self = TestEnvironment(config={'use_pbkdf2': 1})
233+
self.setup()
234+
self.test_round_trip()
235+
self.test_export_gpg()
236+
237+
self = TestEnvironment(config={'use_pbkdf2': 1})
238+
"""
239+
240+
# Test that transcrypt works under a variety of config conditions
241+
basis = {
242+
'cipher': ['aes-256-cbc'],
243+
'password': ['correct horse battery staple'],
244+
'digest': ['md5', 'sha256'],
245+
'use_pbkdf2': ['0', '1'],
246+
'salt_method': ['password', 'configured'],
247+
'config_salt': ['', 'mylittlecustomsalt'],
248+
}
249+
250+
for params in ub.named_product(basis):
251+
config = params.copy()
252+
self = TestEnvironment(config=config)
253+
self.setup()
254+
self.test_round_trip()
255+
self.test_export_gpg()
256+
257+
258+
if __name__ == '__main__':
259+
"""
260+
CommandLine:
261+
python ~/code/transcrypt/tests/test_transcrypt.py
262+
"""
263+
run_tests()

0 commit comments

Comments
 (0)