diff --git a/changelogs/fragments/829-luks_device-passphrase-base64.yml b/changelogs/fragments/829-luks_device-passphrase-base64.yml new file mode 100644 index 000000000..b46fdf2be --- /dev/null +++ b/changelogs/fragments/829-luks_device-passphrase-base64.yml @@ -0,0 +1,3 @@ +minor_changes: + - "luks_device - allow to provide passphrases base64-encoded + (https://github.com/ansible-collections/community.crypto/issues/827, https://github.com/ansible-collections/community.crypto/pull/829)." diff --git a/plugins/modules/luks_device.py b/plugins/modules/luks_device.py index cf0be06ca..dbc1b0020 100644 --- a/plugins/modules/luks_device.py +++ b/plugins/modules/luks_device.py @@ -61,8 +61,25 @@ description: - Used to unlock the container. Either a O(passphrase) or a O(keyfile) is needed for most of the operations. Parameter value is a string with the passphrase. + - B(Note) that the passphrase must be UTF-8 encoded text. If you want to use arbitrary binary data, or text using + another encoding, use the O(passphrase_encoding) option and provide the passphrase Base64 encoded. type: str version_added: '1.0.0' + passphrase_encoding: + description: + - Determine how passphrases are provided to parameters such as O(passphrase), O(new_passphrase), and O(remove_passphrase). + - Please note that binary passphrases cannot contain all possible binary octets. For example, a newline (0x0A) + cannot be used since it indicates that the passphrase is over. If you want to use arbitrary binary data, you must + use keyfiles. + type: str + default: text + choices: + text: + - The passphrase is provided as UTF-8 encoded text. + base64: + - The passphrase is provided as Base64 encoded bytes. + - Use the P(ansible.builtin.b64encode#filter) filter to Base64-encode binary data. + version_added: 2.23.0 keyslot: description: - Adds the O(keyfile) or O(passphrase) to a specific keyslot when creating a new container on O(device). Parameter value @@ -91,6 +108,8 @@ LUKS container supports up to 8 keyslots. Parameter value is a string with the new passphrase. - NOTE that adding additional passphrase is idempotent only since community.crypto 1.4.0. For older versions, a new keyslot will be used even if another keyslot already exists for this passphrase. + - B(Note) that the passphrase must be UTF-8 encoded text. If you want to use arbitrary binary data, or text using + another encoding, use the O(passphrase_encoding) option and provide the passphrase Base64 encoded. type: str version_added: '1.0.0' new_keyslot: @@ -116,6 +135,8 @@ - NOTE that removing passphrases is idempotent only since community.crypto 1.4.0. For older versions, trying to remove a passphrase which no longer exists results in an error. - NOTE that to remove the last keyslot from a LUKS container, the O(force_remove_last_key) option must be set to V(true). + - B(Note) that the passphrase must be UTF-8 encoded text. If you want to use arbitrary binary data, or text using + another encoding, use the O(passphrase_encoding) option and provide the passphrase Base64 encoded. type: str version_added: '1.0.0' remove_keyslot: @@ -401,7 +422,10 @@ import re import stat +from base64 import b64decode + from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_bytes, to_native RETURN_CODE = 0 STDOUT = 1 @@ -448,9 +472,23 @@ class Handler(object): def __init__(self, module): self._module = module self._lsblk_bin = self._module.get_bin_path('lsblk', True) + self._passphrase_encoding = module.params['passphrase_encoding'] + + def get_passphrase_from_module_params(self, parameter_name): + passphrase = self._module.params[parameter_name] + if passphrase is None: + return None + if self._passphrase_encoding == 'text': + return to_bytes(passphrase) + try: + return b64decode(to_native(passphrase)) + except Exception as exc: + self._module.fail_json("Error while base64-decoding '{parameter_name}': {exc}".format(parameter_name=parameter_name, exc=exc)) def _run_command(self, command, data=None): - return self._module.run_command(command, data=data) + if data is not None: + data += b'\n' + return self._module.run_command(command, data=data, binary_data=True) def get_device_by_uuid(self, uuid): ''' Returns the device that holds UUID passed by user @@ -673,7 +711,7 @@ def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile, else: data.extend([new_passphrase, new_passphrase]) - result = self._run_command(args, data='\n'.join(data) or None) + result = self._run_command(args, data=b'\n'.join(data) or None) if result[RETURN_CODE] != 0: raise ValueError('Error while adding new LUKS keyslot to %s: %s' % (device, result[STDERR])) @@ -863,10 +901,17 @@ def luks_add_key(self): self._module.fail_json(msg="Contradiction in setup: Asking to " "add a key to absent LUKS.") - key_present = self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase']) + key_present = self._crypthandler.luks_test_key( + self.device, + self._module.params['new_keyfile'], + self.get_passphrase_from_module_params('new_passphrase'), + ) if self._module.params['new_keyslot'] is not None: - key_present_slot = self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'], - self._module.params['new_keyslot']) + key_present_slot = self._crypthandler.luks_test_key( + self.device, self._module.params['new_keyfile'], + self.get_passphrase_from_module_params('new_passphrase'), + self._module.params['new_keyslot'], + ) if key_present and not key_present_slot: self._module.fail_json(msg="Trying to add key that is already present in another slot") @@ -887,13 +932,25 @@ def luks_remove_key(self): if self._module.params['remove_keyslot'] is not None: if not self._crypthandler.is_luks_slot_set(self.device, self._module.params['remove_keyslot']): return False - result = self._crypthandler.luks_test_key(self.device, self._module.params['keyfile'], self._module.params['passphrase']) - if self._crypthandler.luks_test_key(self.device, self._module.params['keyfile'], self._module.params['passphrase'], - self._module.params['remove_keyslot']): + result = self._crypthandler.luks_test_key( + self.device, + self._module.params['keyfile'], + self.get_passphrase_from_module_params('passphrase'), + ) + if self._crypthandler.luks_test_key( + self.device, + self._module.params['keyfile'], + self.get_passphrase_from_module_params('passphrase'), + self._module.params['remove_keyslot'], + ): self._module.fail_json(msg='Cannot remove keyslot with keyfile or passphrase in same slot.') return result - return self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase']) + return self._crypthandler.luks_test_key( + self.device, + self._module.params['remove_keyfile'], + self.get_passphrase_from_module_params('remove_passphrase'), + ) def luks_remove(self): return (self.device is not None and @@ -926,6 +983,7 @@ def run_module(): passphrase=dict(type='str', no_log=True), new_passphrase=dict(type='str', no_log=True), remove_passphrase=dict(type='str', no_log=True), + passphrase_encoding=dict(type='str', default='text', choices=['text', 'base64']), keyslot=dict(type='int', no_log=False), new_keyslot=dict(type='int', no_log=False), remove_keyslot=dict(type='int', no_log=False), @@ -1007,16 +1065,17 @@ def run_module(): if conditions.luks_create(): if not module.check_mode: try: - crypt.run_luks_create(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - module.params['keyslot'], - module.params['keysize'], - module.params['cipher'], - module.params['hash'], - module.params['sector_size'], - module.params['pbkdf'], - ) + crypt.run_luks_create( + conditions.device, + module.params['keyfile'], + conditions.get_passphrase_from_module_params('passphrase'), + module.params['keyslot'], + module.params['keysize'], + module.params['cipher'], + module.params['hash'], + module.params['sector_size'], + module.params['pbkdf'], + ) except ValueError as e: module.fail_json(msg="luks_device error: %s" % e) result['changed'] = True @@ -1038,16 +1097,18 @@ def run_module(): module.fail_json(msg="luks_device error: %s" % e) if not module.check_mode: try: - crypt.run_luks_open(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - module.params['perf_same_cpu_crypt'], - module.params['perf_submit_from_crypt_cpus'], - module.params['perf_no_read_workqueue'], - module.params['perf_no_write_workqueue'], - module.params['persistent'], - module.params['allow_discards'], - name) + crypt.run_luks_open( + conditions.device, + module.params['keyfile'], + conditions.get_passphrase_from_module_params('passphrase'), + module.params['perf_same_cpu_crypt'], + module.params['perf_submit_from_crypt_cpus'], + module.params['perf_no_read_workqueue'], + module.params['perf_no_write_workqueue'], + module.params['persistent'], + module.params['allow_discards'], + name, + ) except ValueError as e: module.fail_json(msg="luks_device error: %s" % e) result['name'] = name @@ -1079,13 +1140,15 @@ def run_module(): if conditions.luks_add_key(): if not module.check_mode: try: - crypt.run_luks_add_key(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - module.params['new_keyfile'], - module.params['new_passphrase'], - module.params['new_keyslot'], - module.params['pbkdf']) + crypt.run_luks_add_key( + conditions.device, + module.params['keyfile'], + conditions.get_passphrase_from_module_params('passphrase'), + module.params['new_keyfile'], + conditions.get_passphrase_from_module_params('new_passphrase'), + module.params['new_keyslot'], + module.params['pbkdf'], + ) except ValueError as e: module.fail_json(msg="luks_device error: %s" % e) result['changed'] = True @@ -1097,11 +1160,13 @@ def run_module(): if not module.check_mode: try: last_key = module.params['force_remove_last_key'] - crypt.run_luks_remove_key(conditions.device, - module.params['remove_keyfile'], - module.params['remove_passphrase'], - module.params['remove_keyslot'], - force_remove_last_key=last_key) + crypt.run_luks_remove_key( + conditions.device, + module.params['remove_keyfile'], + conditions.get_passphrase_from_module_params('remove_passphrase'), + module.params['remove_keyslot'], + force_remove_last_key=last_key, + ) except ValueError as e: module.fail_json(msg="luks_device error: %s" % e) result['changed'] = True diff --git a/tests/integration/targets/luks_device/tasks/tests/passphrase.yml b/tests/integration/targets/luks_device/tasks/tests/passphrase.yml index 19551eccd..5aca10644 100644 --- a/tests/integration/targets/luks_device/tasks/tests/passphrase.yml +++ b/tests/integration/targets/luks_device/tasks/tests/passphrase.yml @@ -39,7 +39,9 @@ luks_device: device: "{{ cryptfile_device }}" state: opened - passphrase: "{{ cryptfile_passphrase1 }}" + # Encode passphrase with Base64 to test passphrase_encoding + passphrase: "{{ cryptfile_passphrase1 | b64encode }}" + passphrase_encoding: base64 become: true ignore_errors: true register: open_try diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index a2980b921..bf407bf5c 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -9,6 +9,7 @@ plugins/modules/acme_account_info.py validate-modules:return-syntax-error plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 07a994f88..443b08f7f 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -8,6 +8,7 @@ plugins/modules/acme_account_info.py validate-modules:return-syntax-error plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 54b6198ba..5efa71c13 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -3,6 +3,7 @@ plugins/modules/acme_account_info.py validate-modules:return-syntax-error plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 389b3f533..6e3b04e38 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -2,6 +2,7 @@ plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 389b3f533..6e3b04e38 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -2,6 +2,7 @@ plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index ca127b4fd..c5ae58af3 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -1,3 +1,4 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen +plugins/modules/luks_device.py validate-modules:invalid-documentation tests/ee/roles/smoke/library/smoke_ipaddress.py shebang tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 9ffe1e998..a4cdbdb3c 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -1,2 +1,3 @@ +plugins/modules/luks_device.py validate-modules:invalid-documentation tests/ee/roles/smoke/library/smoke_ipaddress.py shebang tests/ee/roles/smoke/library/smoke_pyyaml.py shebang diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index c5b2bb0bf..4639a5458 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -8,6 +8,7 @@ docs/docsite/rst/guide_selfsigned.rst rstcheck plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error plugins/modules/ecs_certificate.py validate-modules:invalid-documentation plugins/modules/get_certificate.py validate-modules:invalid-documentation +plugins/modules/luks_device.py validate-modules:invalid-documentation plugins/modules/openssh_cert.py validate-modules:invalid-documentation plugins/modules/openssl_csr.py validate-modules:invalid-documentation plugins/modules/openssl_csr_info.py validate-modules:invalid-documentation diff --git a/tests/unit/plugins/modules/test_luks_device.py b/tests/unit/plugins/modules/test_luks_device.py index 371001827..481b47000 100644 --- a/tests/unit/plugins/modules/test_luks_device.py +++ b/tests/unit/plugins/modules/test_luks_device.py @@ -25,6 +25,7 @@ def get_bin_path(self, command, dummy): def test_generate_luks_name(monkeypatch): module = DummyModule() + module.params["passphrase_encoding"] = "text" monkeypatch.setattr(luks_device.Handler, "_run_command", lambda x, y: [0, "UUID", ""]) crypt = luks_device.CryptHandler(module) @@ -33,6 +34,7 @@ def test_generate_luks_name(monkeypatch): def test_get_container_name_by_device(monkeypatch): module = DummyModule() + module.params["passphrase_encoding"] = "text" monkeypatch.setattr(luks_device.Handler, "_run_command", lambda x, y: [0, "crypt container_name", ""]) crypt = luks_device.CryptHandler(module) @@ -41,6 +43,7 @@ def test_get_container_name_by_device(monkeypatch): def test_get_container_device_by_name(monkeypatch): module = DummyModule() + module.params["passphrase_encoding"] = "text" monkeypatch.setattr(luks_device.Handler, "_run_command", lambda x, y: [0, "device: /dev/luksdevice", ""]) crypt = luks_device.CryptHandler(module) @@ -54,6 +57,7 @@ def run_command_check(self, command): return [0, "", ""] module = DummyModule() + module.params["passphrase_encoding"] = "text" monkeypatch.setattr(luks_device.CryptHandler, "get_container_name_by_device", lambda x, y: None) @@ -171,6 +175,7 @@ def test_luks_create(device, keyfile, passphrase, state, is_luks, label, cipher, module.params["device"] = device module.params["keyfile"] = keyfile module.params["passphrase"] = passphrase + module.params["passphrase_encoding"] = "text" module.params["state"] = state module.params["label"] = label module.params["cipher"] = cipher @@ -196,6 +201,7 @@ def test_luks_remove(device, state, is_luks, expected, monkeypatch): module = DummyModule() module.params["device"] = device + module.params["passphrase_encoding"] = "text" module.params["state"] = state monkeypatch.setattr(luks_device.CryptHandler, "is_luks", @@ -218,6 +224,7 @@ def test_luks_open(device, keyfile, passphrase, state, name, name_by_dev, module.params["device"] = device module.params["keyfile"] = keyfile module.params["passphrase"] = passphrase + module.params["passphrase_encoding"] = "text" module.params["state"] = state module.params["name"] = name @@ -246,6 +253,7 @@ def test_luks_close(device, dev_by_name, name, name_by_dev, state, module = DummyModule() module.params["device"] = device module.params["name"] = name + module.params["passphrase_encoding"] = "text" module.params["state"] = state module.params["label"] = label @@ -273,6 +281,7 @@ def test_luks_add_key(device, keyfile, passphrase, new_keyfile, new_passphrase, module.params["device"] = device module.params["keyfile"] = keyfile module.params["passphrase"] = passphrase + module.params["passphrase_encoding"] = "text" module.params["new_keyfile"] = new_keyfile module.params["new_passphrase"] = new_passphrase module.params["new_keyslot"] = None @@ -301,6 +310,7 @@ def test_luks_remove_key(device, remove_keyfile, remove_passphrase, remove_keysl module = DummyModule() module.params["device"] = device + module.params["passphrase_encoding"] = "text" module.params["remove_keyfile"] = remove_keyfile module.params["remove_passphrase"] = remove_passphrase module.params["remove_keyslot"] = remove_keyslot