Test with GnuPG 2 (#179)

Take advantage of pinentry-mock to obtain passphrases, instead of using
"expect" (which requires GnuPG 1).
This commit is contained in:
Tim Byrne 2019-12-02 17:57:57 -06:00
parent e5ff95d09c
commit 5d484ca825
No known key found for this signature in database
GPG Key ID: 14DB4FC2465A4B12
1 changed files with 111 additions and 64 deletions

View File

@ -2,6 +2,7 @@
import os import os
import pipes import pipes
import time
import pytest import pytest
KEY_FILE = 'test/test_key' KEY_FILE = 'test/test_key'
@ -13,26 +14,50 @@ PASSPHRASE = 'ExamplePassword'
pytestmark = pytest.mark.usefixtures('config_git') pytestmark = pytest.mark.usefixtures('config_git')
def add_asymmetric_key(): def add_asymmetric_key(runner, gnupg):
"""Add asymmetric key""" """Add asymmetric key"""
os.system(f'gpg --import {pipes.quote(KEY_FILE)}') env = os.environ.copy()
os.system(f'gpg --import-ownertrust < {pipes.quote(KEY_TRUST)}') env['GNUPGHOME'] = gnupg.home
runner(
['gpg', '--import', pipes.quote(KEY_FILE)],
env=env,
shell=True,
)
runner(
['gpg', '--import-ownertrust', '<', pipes.quote(KEY_TRUST)],
env=env,
shell=True,
)
def remove_asymmetric_key(): def remove_asymmetric_key(runner, gnupg):
"""Remove asymmetric key""" """Remove asymmetric key"""
os.system( env = os.environ.copy()
f'gpg --batch --yes ' env['GNUPGHOME'] = gnupg.home
f'--delete-secret-keys {pipes.quote(KEY_FINGERPRINT)}') runner(
os.system(f'gpg --batch --yes --delete-key {pipes.quote(KEY_FINGERPRINT)}') [
'gpg', '--batch', '--yes',
'--delete-secret-keys', pipes.quote(KEY_FINGERPRINT)
],
env=env,
shell=True,
)
runner(
[
'gpg', '--batch', '--yes',
'--delete-key', pipes.quote(KEY_FINGERPRINT)
],
env=env,
shell=True,
)
@pytest.fixture @pytest.fixture
def asymmetric_key(): def asymmetric_key(runner, gnupg):
"""Fixture for asymmetric key, removed in teardown""" """Fixture for asymmetric key, removed in teardown"""
add_asymmetric_key() add_asymmetric_key(runner, gnupg)
yield KEY_NAME yield KEY_NAME
remove_asymmetric_key() remove_asymmetric_key(runner, gnupg)
@pytest.fixture @pytest.fixture
@ -95,7 +120,7 @@ def encrypt_targets(yadm_y, paths):
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def decrypt_targets(tmpdir_factory, runner): def decrypt_targets(tmpdir_factory, runner, gnupg):
"""Fixture for setting data to decrypt """Fixture for setting data to decrypt
This fixture: This fixture:
@ -117,17 +142,21 @@ def decrypt_targets(tmpdir_factory, runner):
tmpdir.join('subdir/decrypt3').write('subdir/decrypt3') tmpdir.join('subdir/decrypt3').write('subdir/decrypt3')
expected.append('subdir/decrypt3') expected.append('subdir/decrypt3')
gnupg.pw(PASSPHRASE)
env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
run = runner( run = runner(
['tar', 'cvf', '-'] + ['tar', 'cvf', '-'] +
expected + expected +
['|', 'gpg', '--batch', '--yes', '-c'] + ['|', 'gpg', '--batch', '--yes', '-c'] +
['--passphrase', pipes.quote(PASSPHRASE)] +
['--output', pipes.quote(str(symmetric))], ['--output', pipes.quote(str(symmetric))],
cwd=tmpdir, cwd=tmpdir,
env=env,
shell=True) shell=True)
assert run.success assert run.success
add_asymmetric_key() gnupg.pw('')
add_asymmetric_key(runner, gnupg)
run = runner( run = runner(
['tar', 'cvf', '-'] + ['tar', 'cvf', '-'] +
expected + expected +
@ -135,9 +164,10 @@ def decrypt_targets(tmpdir_factory, runner):
['-r', pipes.quote(KEY_NAME)] + ['-r', pipes.quote(KEY_NAME)] +
['--output', pipes.quote(str(asymmetric))], ['--output', pipes.quote(str(asymmetric))],
cwd=tmpdir, cwd=tmpdir,
env=env,
shell=True) shell=True)
assert run.success assert run.success
remove_asymmetric_key() remove_asymmetric_key(runner, gnupg)
return { return {
'asymmetric': asymmetric, 'asymmetric': asymmetric,
@ -147,8 +177,8 @@ def decrypt_targets(tmpdir_factory, runner):
@pytest.mark.parametrize( @pytest.mark.parametrize(
'mismatched_phrase', [False, True], 'bad_phrase', [False, True],
ids=['matching_phrase', 'mismatched_phrase']) ids=['good_phrase', 'bad_phrase'])
@pytest.mark.parametrize( @pytest.mark.parametrize(
'missing_encrypt', [False, True], 'missing_encrypt', [False, True],
ids=['encrypt_exists', 'encrypt_missing']) ids=['encrypt_exists', 'encrypt_missing'])
@ -157,25 +187,25 @@ def decrypt_targets(tmpdir_factory, runner):
ids=['clean', 'overwrite']) ids=['clean', 'overwrite'])
def test_symmetric_encrypt( def test_symmetric_encrypt(
runner, yadm_y, paths, encrypt_targets, runner, yadm_y, paths, encrypt_targets,
overwrite, missing_encrypt, mismatched_phrase): gnupg, bad_phrase, overwrite, missing_encrypt):
"""Test symmetric encryption""" """Test symmetric encryption"""
if missing_encrypt: if missing_encrypt:
paths.encrypt.remove() paths.encrypt.remove()
matched_phrase = PASSPHRASE if bad_phrase:
if mismatched_phrase: gnupg.pw('')
matched_phrase = 'mismatched' else:
gnupg.pw(PASSPHRASE)
if overwrite: if overwrite:
paths.archive.write('existing archive') paths.archive.write('existing archive')
run = runner(yadm_y('encrypt'), expect=[ env = os.environ.copy()
('passphrase:', PASSPHRASE), env['GNUPGHOME'] = gnupg.home
('passphrase:', matched_phrase), run = runner(yadm_y('encrypt'), env=env)
])
if missing_encrypt or mismatched_phrase: if missing_encrypt or bad_phrase:
assert run.failure assert run.failure
else: else:
assert run.success assert run.success
@ -183,15 +213,16 @@ def test_symmetric_encrypt(
if missing_encrypt: if missing_encrypt:
assert 'does not exist' in run.out assert 'does not exist' in run.out
elif mismatched_phrase: elif bad_phrase:
assert 'invalid passphrase' in run.out assert 'Invalid passphrase' in run.err
else: else:
assert encrypted_data_valid(runner, paths.archive, encrypt_targets) assert encrypted_data_valid(
runner, gnupg, paths.archive, encrypt_targets)
@pytest.mark.parametrize( @pytest.mark.parametrize(
'wrong_phrase', [False, True], 'bad_phrase', [False, True],
ids=['correct_phrase', 'wrong_phrase']) ids=['good_phrase', 'bad_phrase'])
@pytest.mark.parametrize( @pytest.mark.parametrize(
'archive_exists', [True, False], 'archive_exists', [True, False],
ids=['archive_exists', 'archive_missing']) ids=['archive_exists', 'archive_missing'])
@ -199,16 +230,18 @@ def test_symmetric_encrypt(
'dolist', [False, True], 'dolist', [False, True],
ids=['decrypt', 'list']) ids=['decrypt', 'list'])
def test_symmetric_decrypt( def test_symmetric_decrypt(
runner, yadm_y, paths, decrypt_targets, runner, yadm_y, paths, decrypt_targets, gnupg,
dolist, archive_exists, wrong_phrase): dolist, archive_exists, bad_phrase):
"""Test decryption""" """Test decryption"""
# init empty yadm repo # init empty yadm repo
os.system(' '.join(yadm_y('init', '-w', str(paths.work), '-f'))) os.system(' '.join(yadm_y('init', '-w', str(paths.work), '-f')))
phrase = PASSPHRASE if bad_phrase:
if wrong_phrase: gnupg.pw('')
phrase = 'wrong-phrase' time.sleep(1) # allow gpg-agent cache to expire
else:
gnupg.pw(PASSPHRASE)
if archive_exists: if archive_exists:
decrypt_targets['symmetric'].copy(paths.archive) decrypt_targets['symmetric'].copy(paths.archive)
@ -216,15 +249,18 @@ def test_symmetric_decrypt(
# to test overwriting # to test overwriting
paths.work.join('decrypt1').write('pre-existing file') paths.work.join('decrypt1').write('pre-existing file')
env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
args = [] args = []
if dolist: if dolist:
args.append('-l') args.append('-l')
run = runner(yadm_y('decrypt') + args, expect=[('passphrase:', phrase)]) run = runner(yadm_y('decrypt') + args, env=env)
if archive_exists and not wrong_phrase: if archive_exists and not bad_phrase:
assert run.success assert run.success
assert run.err == '' assert 'encrypted with 1 passphrase' in run.err
if dolist: if dolist:
for filename in decrypt_targets['expected']: for filename in decrypt_targets['expected']:
if filename != 'decrypt1': # this one should exist if filename != 'decrypt1': # this one should exist
@ -248,7 +284,7 @@ def test_symmetric_decrypt(
'overwrite', [False, True], 'overwrite', [False, True],
ids=['clean', 'overwrite']) ids=['clean', 'overwrite'])
def test_asymmetric_encrypt( def test_asymmetric_encrypt(
runner, yadm_y, paths, encrypt_targets, runner, yadm_y, paths, encrypt_targets, gnupg,
overwrite, key_exists, ask): overwrite, key_exists, ask):
"""Test asymmetric encryption""" """Test asymmetric encryption"""
@ -264,13 +300,17 @@ def test_asymmetric_encrypt(
paths.archive.write('existing archive') paths.archive.write('existing archive')
if not key_exists: if not key_exists:
remove_asymmetric_key() remove_asymmetric_key(runner, gnupg)
run = runner(yadm_y('encrypt'), expect=expect) env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
run = runner(yadm_y('encrypt'), env=env, expect=expect)
if key_exists: if key_exists:
assert run.success assert run.success
assert encrypted_data_valid(runner, paths.archive, encrypt_targets) assert encrypted_data_valid(
runner, gnupg, paths.archive, encrypt_targets)
else: else:
assert run.failure assert run.failure
assert 'Unable to write' in run.out assert 'Unable to write' in run.out
@ -287,7 +327,7 @@ def test_asymmetric_encrypt(
'dolist', [False, True], 'dolist', [False, True],
ids=['decrypt', 'list']) ids=['decrypt', 'list'])
def test_asymmetric_decrypt( def test_asymmetric_decrypt(
runner, yadm_y, paths, decrypt_targets, runner, yadm_y, paths, decrypt_targets, gnupg,
dolist, key_exists): dolist, key_exists):
"""Test decryption""" """Test decryption"""
@ -300,13 +340,15 @@ def test_asymmetric_decrypt(
paths.work.join('decrypt1').write('pre-existing file') paths.work.join('decrypt1').write('pre-existing file')
if not key_exists: if not key_exists:
remove_asymmetric_key() remove_asymmetric_key(runner, gnupg)
args = [] args = []
if dolist: if dolist:
args.append('-l') args.append('-l')
run = runner(yadm_y('decrypt') + args) env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
run = runner(yadm_y('decrypt') + args, env=env)
if key_exists: if key_exists:
assert run.success assert run.success
@ -327,7 +369,8 @@ def test_asymmetric_decrypt(
'untracked', 'untracked',
[False, 'y', 'n'], [False, 'y', 'n'],
ids=['tracked', 'untracked_answer_y', 'untracked_answer_n']) ids=['tracked', 'untracked_answer_y', 'untracked_answer_n'])
def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked): def test_offer_to_add(
runner, yadm_y, paths, encrypt_targets, gnupg, untracked):
"""Test offer to add encrypted archive """Test offer to add encrypted archive
All the other encryption tests use an archive outside of the work tree. All the other encryption tests use an archive outside of the work tree.
@ -336,10 +379,12 @@ def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked):
""" """
worktree_archive = paths.work.join('worktree-archive.tar.gpg') worktree_archive = paths.work.join('worktree-archive.tar.gpg')
expect = [
('passphrase:', PASSPHRASE), expect = []
('passphrase:', PASSPHRASE),
] gnupg.pw(PASSPHRASE)
env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
if untracked: if untracked:
expect.append(('add it now', untracked)) expect.append(('add it now', untracked))
@ -349,12 +394,14 @@ def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked):
run = runner( run = runner(
yadm_y('encrypt', '--yadm-archive', str(worktree_archive)), yadm_y('encrypt', '--yadm-archive', str(worktree_archive)),
env=env,
expect=expect expect=expect
) )
assert run.success assert run.success
assert run.err == '' assert run.err == ''
assert encrypted_data_valid(runner, worktree_archive, encrypt_targets) assert encrypted_data_valid(
runner, gnupg, worktree_archive, encrypt_targets)
run = runner( run = runner(
yadm_y('status', '--porcelain', '-uall', str(worktree_archive))) yadm_y('status', '--porcelain', '-uall', str(worktree_archive)))
@ -372,22 +419,20 @@ def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked):
assert f'AM {worktree_archive.basename}' in run.out assert f'AM {worktree_archive.basename}' in run.out
def test_encrypt_added_to_exclude(runner, yadm_y, paths): @pytest.mark.usefixtures('ds1_copy')
def test_encrypt_added_to_exclude(runner, yadm_y, paths, gnupg):
"""Confirm that .config/yadm/encrypt is added to exclude""" """Confirm that .config/yadm/encrypt is added to exclude"""
expect = [ gnupg.pw(PASSPHRASE)
('passphrase:', PASSPHRASE), env = os.environ.copy()
('passphrase:', PASSPHRASE), env['GNUPGHOME'] = gnupg.home
]
exclude_file = paths.repo.join('info/exclude') exclude_file = paths.repo.join('info/exclude')
paths.encrypt.write('test-encrypt-data\n') paths.encrypt.write('test-encrypt-data\n')
paths.work.join('test-encrypt-data').write('')
exclude_file.write('original-data', ensure=True) exclude_file.write('original-data', ensure=True)
run = runner( run = runner(yadm_y('encrypt'), env=env)
yadm_y('encrypt'),
expect=expect,
)
assert 'test-encrypt-data' in paths.repo.join('info/exclude').read() assert 'test-encrypt-data' in paths.repo.join('info/exclude').read()
assert 'original-data' in paths.repo.join('info/exclude').read() assert 'original-data' in paths.repo.join('info/exclude').read()
@ -395,14 +440,16 @@ def test_encrypt_added_to_exclude(runner, yadm_y, paths):
assert run.err == '' assert run.err == ''
def encrypted_data_valid(runner, encrypted, expected): def encrypted_data_valid(runner, gnupg, encrypted, expected):
"""Verify encrypted data matches expectations""" """Verify encrypted data matches expectations"""
gnupg.pw(PASSPHRASE)
env = os.environ.copy()
env['GNUPGHOME'] = gnupg.home
run = runner([ run = runner([
'gpg', 'gpg',
'--passphrase', pipes.quote(PASSPHRASE),
'-d', pipes.quote(str(encrypted)), '-d', pipes.quote(str(encrypted)),
'2>/dev/null', '2>/dev/null',
'|', 'tar', 't'], shell=True, report=False) '|', 'tar', 't'], env=env, shell=True, report=False)
file_count = 0 file_count = 0
for filename in run.out.splitlines(): for filename in run.out.splitlines():
if filename.endswith('/'): if filename.endswith('/'):