luks_device - make add/removal of keyfile/passphrase idempotent (#168)

* Update documentation, adjust tests, add changelog fragment.

* Move module unit test to correct place.

* Implement keyfile / passphrase test.
pull/170/head
Felix Fontein 2021-01-03 11:22:41 +01:00 committed by GitHub
parent fb2f3ef2b5
commit ccb25eab36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 13 deletions

View File

@ -0,0 +1,2 @@
minor_changes:
- "luks_device - ``new_keyfile``, ``new_passphrase``, ``remove_keyfile`` and ``remove_passphrase`` are now idempotent (https://github.com/ansible-collections/community.crypto/issues/19, https://github.com/ansible-collections/community.crypto/pull/168)."

View File

@ -81,9 +81,10 @@ options:
Needs I(keyfile) or I(passphrase) option for authorization. Needs I(keyfile) or I(passphrase) option for authorization.
LUKS container supports up to 8 keyslots. Parameter value LUKS container supports up to 8 keyslots. Parameter value
is the path to the keyfile with the passphrase." is the path to the keyfile with the passphrase."
- "NOTE that adding additional keys is *not idempotent*. - "NOTE that adding additional keys is idempotent only since
A new keyslot will be used even if another keyslot already community.crypto 1.4.0. For older versions, a new keyslot
exists for this keyfile." will be used even if another keyslot already exists for this
keyfile."
- "BEWARE that working with keyfiles in plaintext is dangerous. - "BEWARE that working with keyfiles in plaintext is dangerous.
Make sure that they are protected." Make sure that they are protected."
type: path type: path
@ -93,9 +94,9 @@ options:
Needs I(keyfile) or I(passphrase) option for authorization. LUKS Needs I(keyfile) or I(passphrase) option for authorization. LUKS
container supports up to 8 keyslots. Parameter value is a string container supports up to 8 keyslots. Parameter value is a string
with the new passphrase." with the new passphrase."
- "NOTE that adding additional passphrase is *not idempotent*. A - "NOTE that adding additional passphrase is idempotent only since
new keyslot will be used even if another keyslot already exists community.crypto 1.4.0. For older versions, a new keyslot will
for this passphrase." be used even if another keyslot already exists for this passphrase."
type: str type: str
version_added: '1.0.0' version_added: '1.0.0'
remove_keyfile: remove_keyfile:
@ -103,7 +104,8 @@ options:
- "Removes given key from the container on I(device). Does not - "Removes given key from the container on I(device). Does not
remove the keyfile from filesystem. remove the keyfile from filesystem.
Parameter value is the path to the keyfile with the passphrase." Parameter value is the path to the keyfile with the passphrase."
- "NOTE that removing keys is *not idempotent*. Trying to remove - "NOTE that removing keys is idempotent only since
community.crypto 1.4.0. For older versions, trying to remove
a key which no longer exists results in an error." a key which no longer exists results in an error."
- "NOTE that to remove the last key from a LUKS container, the - "NOTE that to remove the last key from a LUKS container, the
I(force_remove_last_key) option must be set to C(yes)." I(force_remove_last_key) option must be set to C(yes)."
@ -114,9 +116,9 @@ options:
description: description:
- "Removes given passphrase from the container on I(device). - "Removes given passphrase from the container on I(device).
Parameter value is a string with the passphrase to remove." Parameter value is a string with the passphrase to remove."
- "NOTE that removing passphrases is I(not - "NOTE that removing passphrases is idempotent only since
idempotent). Trying to remove a passphrase which no longer community.crypto 1.4.0. For older versions, trying to remove
exists results in an error." a passphrase which no longer exists results in an error."
- "NOTE that to remove the last keyslot from a LUKS - "NOTE that to remove the last keyslot from a LUKS
container, the I(force_remove_last_key) option must be set container, the I(force_remove_last_key) option must be set
to C(yes)." to C(yes)."
@ -520,6 +522,28 @@ class CryptHandler(Handler):
raise ValueError('Error while removing LUKS key from %s: %s' raise ValueError('Error while removing LUKS key from %s: %s'
% (device, result[STDERR])) % (device, result[STDERR]))
def luks_test_key(self, device, keyfile, passphrase):
''' Check whether the keyfile or passphrase works.
Raises ValueError when command fails.
'''
data = None
args = [self._cryptsetup_bin, 'luksOpen', '--test-passphrase', device]
if keyfile:
args.extend(['--key-file', keyfile])
else:
data = passphrase
result = self._run_command(args, data=data)
if result[RETURN_CODE] == 0:
return True
for output in (STDOUT, STDERR):
if 'No key available with this passphrase' in result[output]:
return False
raise ValueError('Error while testing whether keyslot exists on %s: %s'
% (device, result[STDERR]))
class ConditionsHandler(Handler): class ConditionsHandler(Handler):
@ -627,7 +651,7 @@ class ConditionsHandler(Handler):
self._module.fail_json(msg="Contradiction in setup: Asking to " self._module.fail_json(msg="Contradiction in setup: Asking to "
"add a key to absent LUKS.") "add a key to absent LUKS.")
return True return not self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'])
def luks_remove_key(self): def luks_remove_key(self):
if (self.device is None or if (self.device is None or
@ -640,7 +664,7 @@ class ConditionsHandler(Handler):
self._module.fail_json(msg="Contradiction in setup: Asking to " self._module.fail_json(msg="Contradiction in setup: Asking to "
"remove a key from absent LUKS.") "remove a key from absent LUKS.")
return True return self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase'])
def luks_remove(self): def luks_remove(self):
return (self.device is not None and return (self.device is not None and

View File

@ -44,6 +44,21 @@
keyfile: "{{ role_path }}/files/keyfile1" keyfile: "{{ role_path }}/files/keyfile1"
new_keyfile: "{{ role_path }}/files/keyfile2" new_keyfile: "{{ role_path }}/files/keyfile2"
become: yes become: yes
register: result_1
- name: Give access to keyfile2 (idempotent)
luks_device:
device: "{{ cryptfile_device }}"
state: closed
keyfile: "{{ role_path }}/files/keyfile1"
new_keyfile: "{{ role_path }}/files/keyfile2"
become: yes
register: result_2
- assert:
that:
- result_1 is changed
- result_2 is not changed
# Access: keyfile1 and keyfile2 # Access: keyfile1 and keyfile2
@ -75,6 +90,21 @@
keyfile: "{{ role_path }}/files/keyfile1" keyfile: "{{ role_path }}/files/keyfile1"
remove_keyfile: "{{ role_path }}/files/keyfile1" remove_keyfile: "{{ role_path }}/files/keyfile1"
become: yes become: yes
register: result_1
- name: Remove access from keyfile1 (idempotent)
luks_device:
device: "{{ cryptfile_device }}"
state: closed
keyfile: "{{ role_path }}/files/keyfile1"
remove_keyfile: "{{ role_path }}/files/keyfile1"
become: yes
register: result_2
- assert:
that:
- result_1 is changed
- result_2 is not changed
# Access: keyfile2 # Access: keyfile2

View File

@ -56,6 +56,21 @@
passphrase: "{{ cryptfile_passphrase1 }}" passphrase: "{{ cryptfile_passphrase1 }}"
new_passphrase: "{{ cryptfile_passphrase2 }}" new_passphrase: "{{ cryptfile_passphrase2 }}"
become: yes become: yes
register: result_1
- name: Give access to passphrase2 (idempotent)
luks_device:
device: "{{ cryptfile_device }}"
state: closed
passphrase: "{{ cryptfile_passphrase1 }}"
new_passphrase: "{{ cryptfile_passphrase2 }}"
become: yes
register: result_2
- assert:
that:
- result_1 is changed
- result_2 is not changed
- name: Open with passphrase2 - name: Open with passphrase2
luks_device: luks_device:
@ -130,6 +145,20 @@
state: closed state: closed
remove_passphrase: "{{ cryptfile_passphrase1 }}" remove_passphrase: "{{ cryptfile_passphrase1 }}"
become: yes become: yes
register: result_1
- name: Remove access for passphrase1 (idempotent)
luks_device:
device: "{{ cryptfile_device }}"
state: closed
remove_passphrase: "{{ cryptfile_passphrase1 }}"
become: yes
register: result_2
- assert:
that:
- result_1 is changed
- result_2 is not changed
- name: Try to open with passphrase1 - name: Try to open with passphrase1
luks_device: luks_device:

View File

@ -275,9 +275,12 @@ def test_luks_add_key(device, keyfile, passphrase, new_keyfile, new_passphrase,
monkeypatch.setattr(luks_device.Handler, "get_device_by_label", monkeypatch.setattr(luks_device.Handler, "get_device_by_label",
lambda x, y: [0, "/dev/dummy", ""]) lambda x, y: [0, "/dev/dummy", ""])
monkeypatch.setattr(luks_device.CryptHandler, "luks_test_key",
lambda x, y, z, w: False)
crypt = luks_device.CryptHandler(module)
try: try:
conditions = luks_device.ConditionsHandler(module, module) conditions = luks_device.ConditionsHandler(module, crypt)
assert conditions.luks_add_key() == expected assert conditions.luks_add_key() == expected
except ValueError: except ValueError:
assert expected == "exception" assert expected == "exception"
@ -301,6 +304,8 @@ def test_luks_remove_key(device, remove_keyfile, remove_passphrase, state,
lambda x, y: [0, "/dev/dummy", ""]) lambda x, y: [0, "/dev/dummy", ""])
monkeypatch.setattr(luks_device.Handler, "_run_command", monkeypatch.setattr(luks_device.Handler, "_run_command",
lambda x, y: [0, device, ""]) lambda x, y: [0, device, ""])
monkeypatch.setattr(luks_device.CryptHandler, "luks_test_key",
lambda x, y, z, w: True)
crypt = luks_device.CryptHandler(module) crypt = luks_device.CryptHandler(module)
try: try: