From c19936170cf8b385687cf40f5a9507d87ae08267 Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Sat, 30 Apr 2022 12:35:07 +0100 Subject: [PATCH] New filter: luks This filter allows you to open, read and write LUKSv1 disk images, compatible with the ones used by dm-crypt and qemu. (cherry picked from commit 468919dce6c5eb57503eacac0f67e5dd87c58e6c) --- TODO | 11 +- configure.ac | 6 +- docs/nbdkit-tls.pod | 1 + filters/luks/Makefile.am | 77 ++ filters/luks/luks.c | 1263 +++++++++++++++++++++++++++ filters/luks/nbdkit-luks-filter.pod | 120 +++ plugins/file/nbdkit-file-plugin.pod | 1 + tests/Makefile.am | 12 + tests/test-luks-copy.sh | 125 +++ tests/test-luks-info.sh | 56 ++ 10 files changed, 1668 insertions(+), 4 deletions(-) create mode 100644 filters/luks/Makefile.am create mode 100644 filters/luks/luks.c create mode 100644 filters/luks/nbdkit-luks-filter.pod create mode 100755 tests/test-luks-copy.sh create mode 100755 tests/test-luks-info.sh diff --git a/TODO b/TODO index 4d2a9796..0f5dc41d 100644 --- a/TODO +++ b/TODO @@ -195,9 +195,6 @@ Suggestions for filters connections. This may even allow a filter to offer a more parallel threading model than the underlying plugin. -* LUKS encrypt/decrypt filter, bonus points if compatible with qemu - LUKS-encrypted disk images - * CBT filter to track dirty blocks. See these links for inspiration: https://www.cloudandheat.com/block-level-data-tracking-using-davice-mappers-dm-era/ https://github.com/qemu/qemu/blob/master/docs/interop/bitmaps.rst @@ -232,6 +229,14 @@ Suggestions for filters could inject a flush after pausing. However this requires that filter background threads have access to the plugin (see above). +nbdkit-luks-filter: + +* This filter should also support LUKSv2 (and so should qemu). + +* There are some missing features: ESSIV, more ciphers. + +* Implement trim and zero if possible. + nbdkit-readahead-filter: * The filter should open a new connection to the plugin per background diff --git a/configure.ac b/configure.ac index a402921b..de85b4da 100644 --- a/configure.ac +++ b/configure.ac @@ -127,6 +127,7 @@ filters="\ ip \ limit \ log \ + luks \ multi-conn \ nocache \ noextents \ @@ -614,8 +615,9 @@ PKG_CHECK_MODULES([GNUTLS], [gnutls >= 3.3.0], [ ], [ AC_MSG_WARN([gnutls not found or < 3.3.0, TLS support will be disabled.]) ]) +AM_CONDITIONAL([HAVE_GNUTLS], [test "x$GNUTLS_LIBS" != "x"]) -AS_IF([test "$GNUTLS_LIBS" != ""],[ +AS_IF([test "x$GNUTLS_LIBS" != "x"],[ AC_MSG_CHECKING([for default TLS session priority string]) AC_ARG_WITH([tls-priority], [AS_HELP_STRING([--with-tls-priority=...], @@ -1383,6 +1385,7 @@ AC_CONFIG_FILES([Makefile filters/ip/Makefile filters/limit/Makefile filters/log/Makefile + filters/luks/Makefile filters/multi-conn/Makefile filters/nocache/Makefile filters/noextents/Makefile @@ -1481,6 +1484,7 @@ echo "Optional filters:" echo feature "ext2" test "x$HAVE_EXT2_TRUE" = "x" feature "gzip" test "x$HAVE_ZLIB_TRUE" = "x" +feature "LUKS" test "x$HAVE_GNUTLS_TRUE" != "x" feature "xz" test "x$HAVE_LIBLZMA_TRUE" = "x" echo diff --git a/docs/nbdkit-tls.pod b/docs/nbdkit-tls.pod index 86f5f984..4d0dc14c 100644 --- a/docs/nbdkit-tls.pod +++ b/docs/nbdkit-tls.pod @@ -364,6 +364,7 @@ More information can be found in L. =head1 SEE ALSO L, +L, L, L, L, diff --git a/filters/luks/Makefile.am b/filters/luks/Makefile.am new file mode 100644 index 00000000..30089621 --- /dev/null +++ b/filters/luks/Makefile.am @@ -0,0 +1,77 @@ +# nbdkit +# Copyright (C) 2019-2022 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Red Hat nor the names of its contributors may be +# used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +include $(top_srcdir)/common-rules.mk + +EXTRA_DIST = nbdkit-luks-filter.pod + +if HAVE_GNUTLS + +filter_LTLIBRARIES = nbdkit-luks-filter.la + +nbdkit_luks_filter_la_SOURCES = \ + luks.c \ + $(top_srcdir)/include/nbdkit-filter.h \ + $(NULL) + +nbdkit_luks_filter_la_CPPFLAGS = \ + -I$(top_srcdir)/include \ + -I$(top_srcdir)/common/include \ + -I$(top_srcdir)/common/utils \ + $(NULL) +nbdkit_luks_filter_la_CFLAGS = \ + $(WARNINGS_CFLAGS) \ + $(GNUTLS_CFLAGS) \ + $(NULL) +nbdkit_luks_filter_la_LIBADD = \ + $(top_builddir)/common/utils/libutils.la \ + $(IMPORT_LIBRARY_ON_WINDOWS) \ + $(GNUTLS_LIBS) \ + $(NULL) +nbdkit_luks_filter_la_LDFLAGS = \ + -module -avoid-version -shared $(NO_UNDEFINED_ON_WINDOWS) \ + -Wl,--version-script=$(top_srcdir)/filters/filters.syms \ + $(NULL) + +if HAVE_POD + +man_MANS = nbdkit-luks-filter.1 +CLEANFILES += $(man_MANS) + +nbdkit-luks-filter.1: nbdkit-luks-filter.pod \ + $(top_builddir)/podwrapper.pl + $(PODWRAPPER) --section=1 --man $@ \ + --html $(top_builddir)/html/$@.html \ + $< + +endif HAVE_POD + +endif diff --git a/filters/luks/luks.c b/filters/luks/luks.c new file mode 100644 index 00000000..706a9bd2 --- /dev/null +++ b/filters/luks/luks.c @@ -0,0 +1,1263 @@ +/* nbdkit + * Copyright (C) 2018-2022 Red Hat Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Red Hat nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "byte-swapping.h" +#include "cleanup.h" +#include "isaligned.h" +#include "minmax.h" +#include "rounding.h" + +/* LUKSv1 constants. */ +#define LUKS_MAGIC { 'L', 'U', 'K', 'S', 0xBA, 0xBE } +#define LUKS_MAGIC_LEN 6 +#define LUKS_DIGESTSIZE 20 +#define LUKS_SALTSIZE 32 +#define LUKS_NUMKEYS 8 +#define LUKS_KEY_DISABLED 0x0000DEAD +#define LUKS_KEY_ENABLED 0x00AC71F3 +#define LUKS_STRIPES 4000 +#define LUKS_ALIGN_KEYSLOTS 4096 +#define LUKS_SECTOR_SIZE 512 + +/* Key slot. */ +struct luks_keyslot { + uint32_t active; /* LUKS_KEY_DISABLED|LUKS_KEY_ENABLED */ + uint32_t password_iterations; + char password_salt[LUKS_SALTSIZE]; + uint32_t key_material_offset; + uint32_t stripes; +} __attribute__((__packed__)); + +/* LUKS superblock. */ +struct luks_phdr { + char magic[LUKS_MAGIC_LEN]; /* LUKS_MAGIC */ + uint16_t version; /* Only 1 is supported. */ + char cipher_name[32]; + char cipher_mode[32]; + char hash_spec[32]; + uint32_t payload_offset; + uint32_t master_key_len; + uint8_t master_key_digest[LUKS_DIGESTSIZE]; + uint8_t master_key_salt[LUKS_SALTSIZE]; + uint32_t master_key_digest_iterations; + uint8_t uuid[40]; + + struct luks_keyslot keyslot[LUKS_NUMKEYS]; /* Key slots. */ +} __attribute__((__packed__)); + +static char *passphrase = NULL; + +static void +luks_unload (void) +{ + /* XXX We should really store the passphrase (and master key) + * in mlock-ed memory. + */ + if (passphrase) { + memset (passphrase, 0, strlen (passphrase)); + free (passphrase); + } +} + +static int +luks_thread_model (void) +{ + return NBDKIT_THREAD_MODEL_PARALLEL; +} + +static int +luks_config (nbdkit_next_config *next, nbdkit_backend *nxdata, + const char *key, const char *value) +{ + if (strcmp (key, "passphrase") == 0) { + if (nbdkit_read_password (value, &passphrase) == -1) + return -1; + return 0; + } + + return next (nxdata, key, value); +} + +static int +luks_config_complete (nbdkit_next_config_complete *next, nbdkit_backend *nxdata) +{ + if (passphrase == NULL) { + nbdkit_error ("LUKS \"passphrase\" parameter is missing"); + return -1; + } + return next (nxdata); +} + +#define luks_config_help \ + "passphrase= Secret passphrase." + +enum cipher_mode { + CIPHER_MODE_ECB, CIPHER_MODE_CBC, CIPHER_MODE_XTS, CIPHER_MODE_CTR, +}; + +static enum cipher_mode +lookup_cipher_mode (const char *str) +{ + if (strcmp (str, "ecb") == 0) + return CIPHER_MODE_ECB; + if (strcmp (str, "cbc") == 0) + return CIPHER_MODE_CBC; + if (strcmp (str, "xts") == 0) + return CIPHER_MODE_XTS; + if (strcmp (str, "ctr") == 0) + return CIPHER_MODE_CTR; + nbdkit_error ("unknown cipher mode: %s " + "(expecting \"ecb\", \"cbc\", \"xts\" or \"ctr\")", str); + return -1; +} + +static const char * +cipher_mode_to_string (enum cipher_mode v) +{ + switch (v) { + case CIPHER_MODE_ECB: return "ecb"; + case CIPHER_MODE_CBC: return "cbc"; + case CIPHER_MODE_XTS: return "xts"; + case CIPHER_MODE_CTR: return "ctr"; + default: abort (); + } +} + +enum ivgen { + IVGEN_PLAIN, IVGEN_PLAIN64, /* IVGEN_ESSIV, */ +}; + +static enum ivgen +lookup_ivgen (const char *str) +{ + if (strcmp (str, "plain") == 0) + return IVGEN_PLAIN; + if (strcmp (str, "plain64") == 0) + return IVGEN_PLAIN64; +/* + if (strcmp (str, "essiv") == 0) + return IVGEN_ESSIV; +*/ + nbdkit_error ("unknown IV generation algorithm: %s " + "(expecting \"plain\", \"plain64\" etc)", str); + return -1; +} + +static const char * +ivgen_to_string (enum ivgen v) +{ + switch (v) { + case IVGEN_PLAIN: return "plain"; + case IVGEN_PLAIN64: return "plain64"; + /*case IVGEN_ESSIV: return "essiv";*/ + default: abort (); + } +} + +static void +calculate_iv (enum ivgen v, uint8_t *iv, size_t ivlen, uint64_t sector) +{ + size_t prefixlen; + uint32_t sector32; + + switch (v) { + case IVGEN_PLAIN: + prefixlen = 4; /* 32 bits */ + if (prefixlen > ivlen) + prefixlen = ivlen; + sector32 = (uint32_t) sector; /* truncate to only lower bits */ + sector32 = htole32 (sector32); + memcpy (iv, §or32, prefixlen); + memset (iv + prefixlen, 0, ivlen - prefixlen); + break; + + case IVGEN_PLAIN64: + prefixlen = 8; /* 64 bits */ + if (prefixlen > ivlen) + prefixlen = ivlen; + sector = htole64 (sector); + memcpy (iv, §or, prefixlen); + memset (iv + prefixlen, 0, ivlen - prefixlen); + break; + + /*case IVGEN_ESSIV:*/ + default: abort (); + } +} + +enum cipher_alg { + CIPHER_ALG_AES_128, CIPHER_ALG_AES_192, CIPHER_ALG_AES_256, +}; + +static enum cipher_alg +lookup_cipher_alg (const char *str, enum cipher_mode mode, int key_bytes) +{ + if (mode == CIPHER_MODE_XTS) + key_bytes /= 2; + + if (strcmp (str, "aes") == 0) { + if (key_bytes == 16) + return CIPHER_ALG_AES_128; + if (key_bytes == 24) + return CIPHER_ALG_AES_192; + if (key_bytes == 32) + return CIPHER_ALG_AES_256; + } + nbdkit_error ("unknown cipher algorithm: %s (expecting \"aes\", etc)", str); + return -1; +} + +static const char * +cipher_alg_to_string (enum cipher_alg v) +{ + switch (v) { + case CIPHER_ALG_AES_128: return "aes-128"; + case CIPHER_ALG_AES_192: return "aes-192"; + case CIPHER_ALG_AES_256: return "aes-256"; + default: abort (); + } +} + +#if 0 +static int +cipher_alg_key_bytes (enum cipher_alg v) +{ + switch (v) { + case CIPHER_ALG_AES_128: return 16; + case CIPHER_ALG_AES_192: return 24; + case CIPHER_ALG_AES_256: return 32; + default: abort (); + } +} +#endif + +static int +cipher_alg_iv_len (enum cipher_alg v, enum cipher_mode mode) +{ + if (CIPHER_MODE_ECB) + return 0; /* Don't need an IV in this mode. */ + + switch (v) { + case CIPHER_ALG_AES_128: + case CIPHER_ALG_AES_192: + case CIPHER_ALG_AES_256: + return 16; + default: abort (); + } +} + +static gnutls_digest_algorithm_t +lookup_hash (const char *str) +{ + if (strcmp (str, "md5") == 0) + return GNUTLS_DIG_MD5; + if (strcmp (str, "sha1") == 0) + return GNUTLS_DIG_SHA1; + if (strcmp (str, "sha224") == 0) + return GNUTLS_DIG_SHA224; + if (strcmp (str, "sha256") == 0) + return GNUTLS_DIG_SHA256; + if (strcmp (str, "sha384") == 0) + return GNUTLS_DIG_SHA384; + if (strcmp (str, "sha512") == 0) + return GNUTLS_DIG_SHA512; + if (strcmp (str, "ripemd160") == 0) + return GNUTLS_DIG_RMD160; + nbdkit_error ("unknown hash algorithm: %s " + "(expecting \"md5\", \"sha1\", \"sha224\", etc)", str); + return -1; +} + +static const char * +hash_to_string (gnutls_digest_algorithm_t v) +{ + switch (v) { + case GNUTLS_DIG_UNKNOWN: return "unknown"; + case GNUTLS_DIG_MD5: return "md5"; + case GNUTLS_DIG_SHA1: return "sha1"; + case GNUTLS_DIG_SHA224: return "sha224"; + case GNUTLS_DIG_SHA256: return "sha256"; + case GNUTLS_DIG_SHA384: return "sha384"; + case GNUTLS_DIG_SHA512: return "sha512"; + case GNUTLS_DIG_RMD160: return "ripemd160"; + default: abort (); + } +} + +#if 0 +/* See qemu & dm-crypt implementations for an explanation of what's + * going on here. + */ +static enum cipher_alg +lookup_essiv_cipher (enum cipher_alg cipher_alg, + gnutls_digest_algorithm_t ivgen_hash_alg) +{ + int digest_bytes = gnutls_hash_get_len (ivgen_hash_alg); + int key_bytes = cipher_alg_key_bytes (cipher_alg); + + if (digest_bytes == key_bytes) + return cipher_alg; + + switch (cipher_alg) { + case CIPHER_ALG_AES_128: + case CIPHER_ALG_AES_192: + case CIPHER_ALG_AES_256: + if (digest_bytes == 16) return CIPHER_ALG_AES_128; + if (digest_bytes == 24) return CIPHER_ALG_AES_192; + if (digest_bytes == 32) return CIPHER_ALG_AES_256; + nbdkit_error ("no %s cipher available with key size %d", + "AES", digest_bytes); + return -1; + default: + nbdkit_error ("ESSIV does not support cipher %s", + cipher_alg_to_string (cipher_alg)); + return -1; + } +} +#endif + +/* Per-connection handle. */ +struct handle { + /* LUKS header, if necessary byte-swapped into host order. */ + struct luks_phdr phdr; + + /* Decoded algorithm etc. */ + enum cipher_alg cipher_alg; + enum cipher_mode cipher_mode; + gnutls_digest_algorithm_t hash_alg; + enum ivgen ivgen_alg; + gnutls_digest_algorithm_t ivgen_hash_alg; + enum cipher_alg ivgen_cipher_alg; + + /* GnuTLS algorithm. */ + gnutls_cipher_algorithm_t gnutls_cipher; + + /* If we managed to decrypt one of the keyslots using the passphrase + * then this contains the master key, otherwise NULL. + */ + uint8_t *masterkey; +}; + +static void * +luks_open (nbdkit_next_open *next, nbdkit_context *nxdata, + int readonly, const char *exportname, int is_tls) +{ + struct handle *h; + + if (next (nxdata, readonly, exportname) == -1) + return NULL; + + h = calloc (1, sizeof *h); + if (h == NULL) { + nbdkit_error ("calloc: %m"); + return NULL; + } + + return h; +} + +static void +luks_close (void *handle) +{ + struct handle *h = handle; + + if (h->masterkey) { + memset (h->masterkey, 0, h->phdr.master_key_len); + free (h->masterkey); + } + free (h); +} + +/* Perform decryption of a block of data in memory. */ +static int +do_decrypt (struct handle *h, gnutls_cipher_hd_t cipher, + uint64_t offset, uint8_t *buf, size_t len) +{ + const size_t ivlen = cipher_alg_iv_len (h->cipher_alg, h->cipher_mode); + uint64_t sector = offset / LUKS_SECTOR_SIZE; + CLEANUP_FREE uint8_t *iv = malloc (ivlen); + int r; + + assert (IS_ALIGNED (offset, LUKS_SECTOR_SIZE)); + assert (IS_ALIGNED (len, LUKS_SECTOR_SIZE)); + + while (len) { + calculate_iv (h->ivgen_alg, iv, ivlen, sector); + gnutls_cipher_set_iv (cipher, iv, ivlen); + r = gnutls_cipher_decrypt2 (cipher, + buf, LUKS_SECTOR_SIZE, /* ciphertext */ + buf, LUKS_SECTOR_SIZE /* plaintext */); + if (r != 0) { + nbdkit_error ("gnutls_cipher_decrypt2: %s", gnutls_strerror (r)); + return -1; + } + + buf += LUKS_SECTOR_SIZE; + offset += LUKS_SECTOR_SIZE; + len -= LUKS_SECTOR_SIZE; + sector++; + } + + return 0; +} + +/* Perform encryption of a block of data in memory. */ +static int +do_encrypt (struct handle *h, gnutls_cipher_hd_t cipher, + uint64_t offset, uint8_t *buf, size_t len) +{ + const size_t ivlen = cipher_alg_iv_len (h->cipher_alg, h->cipher_mode); + uint64_t sector = offset / LUKS_SECTOR_SIZE; + CLEANUP_FREE uint8_t *iv = malloc (ivlen); + int r; + + assert (IS_ALIGNED (offset, LUKS_SECTOR_SIZE)); + assert (IS_ALIGNED (len, LUKS_SECTOR_SIZE)); + + while (len) { + calculate_iv (h->ivgen_alg, iv, ivlen, sector); + gnutls_cipher_set_iv (cipher, iv, ivlen); + r = gnutls_cipher_encrypt2 (cipher, + buf, LUKS_SECTOR_SIZE, /* plaintext */ + buf, LUKS_SECTOR_SIZE /* ciphertext */); + if (r != 0) { + nbdkit_error ("gnutls_cipher_decrypt2: %s", gnutls_strerror (r)); + return -1; + } + + buf += LUKS_SECTOR_SIZE; + offset += LUKS_SECTOR_SIZE; + len -= LUKS_SECTOR_SIZE; + sector++; + } + + return 0; +} + +/* Parse the header fields containing cipher algorithm, mode, etc. */ +static int +parse_cipher_strings (struct handle *h) +{ + char cipher_name[33], cipher_mode[33], hash_spec[33]; + char *ivgen, *ivhash; + + /* Copy the header fields locally and ensure they are \0 terminated. */ + memcpy (cipher_name, h->phdr.cipher_name, 32); + cipher_name[32] = 0; + memcpy (cipher_mode, h->phdr.cipher_mode, 32); + cipher_mode[32] = 0; + memcpy (hash_spec, h->phdr.hash_spec, 32); + hash_spec[32] = 0; + + nbdkit_debug ("LUKS v%" PRIu16 " cipher: %s mode: %s hash: %s " + "master key: %" PRIu32 " bits", + h->phdr.version, cipher_name, cipher_mode, hash_spec, + h->phdr.master_key_len * 8); + + /* The cipher_mode header has the form: "ciphermode-ivgen[:ivhash]" + * QEmu writes: "xts-plain64" + */ + ivgen = strchr (cipher_mode, '-'); + if (!ivgen) { + nbdkit_error ("incorrect cipher_mode header, " + "expecting mode-ivgenerator but got \"%s\"", cipher_mode); + return -1; + } + *ivgen = '\0'; + ivgen++; + + ivhash = strchr (ivgen, ':'); + if (!ivhash) + h->ivgen_hash_alg = GNUTLS_DIG_UNKNOWN; + else { + *ivhash = '\0'; + ivhash++; + + h->ivgen_hash_alg = lookup_hash (ivhash); + if (h->ivgen_hash_alg == -1) + return -1; + } + + h->cipher_mode = lookup_cipher_mode (cipher_mode); + if (h->cipher_mode == -1) + return -1; + + h->cipher_alg = lookup_cipher_alg (cipher_name, h->cipher_mode, + h->phdr.master_key_len); + if (h->cipher_alg == -1) + return -1; + + h->hash_alg = lookup_hash (hash_spec); + if (h->hash_alg == -1) + return -1; + + h->ivgen_alg = lookup_ivgen (ivgen); + if (h->ivgen_alg == -1) + return -1; + +#if 0 + if (h->ivgen_alg == IVGEN_ESSIV) { + if (!ivhash) { + nbdkit_error ("incorrect IV generator hash specification"); + return -1; + } + h->ivgen_cipher_alg = lookup_essiv_cipher (h->cipher_alg, + h->ivgen_hash_alg); + if (h->ivgen_cipher_alg == -1) + return -1; + } + else +#endif + h->ivgen_cipher_alg = h->cipher_alg; + + nbdkit_debug ("LUKS parsed ciphers: %s %s %s %s %s %s", + cipher_alg_to_string (h->cipher_alg), + cipher_mode_to_string (h->cipher_mode), + hash_to_string (h->hash_alg), + ivgen_to_string (h->ivgen_alg), + hash_to_string (h->ivgen_hash_alg), + cipher_alg_to_string (h->ivgen_cipher_alg)); + + /* GnuTLS combines cipher and block mode into a single value. Not + * all possible combinations are available in GnuTLS. See: + * https://www.gnutls.org/manual/html_node/Supported-ciphersuites.html + */ + h->gnutls_cipher = GNUTLS_CIPHER_NULL; + switch (h->cipher_mode) { + case CIPHER_MODE_XTS: + switch (h->cipher_alg) { + case CIPHER_ALG_AES_128: + h->gnutls_cipher = GNUTLS_CIPHER_AES_128_XTS; + break; + case CIPHER_ALG_AES_256: + h->gnutls_cipher = GNUTLS_CIPHER_AES_256_XTS; + break; + default: break; + } + break; + case CIPHER_MODE_CBC: + switch (h->cipher_alg) { + case CIPHER_ALG_AES_128: + h->gnutls_cipher = GNUTLS_CIPHER_AES_128_CBC; + break; + case CIPHER_ALG_AES_192: + h->gnutls_cipher = GNUTLS_CIPHER_AES_192_CBC; + break; + case CIPHER_ALG_AES_256: + h->gnutls_cipher = GNUTLS_CIPHER_AES_256_CBC; + break; + default: break; + } + default: break; + } + if (h->gnutls_cipher == GNUTLS_CIPHER_NULL) { + nbdkit_error ("cipher algorithm %s in mode %s is not supported by GnuTLS", + cipher_alg_to_string (h->cipher_alg), + cipher_mode_to_string (h->cipher_mode)); + return -1; + } + + return 0; +} + +/* Anti-Forensic merge operation. */ +static void +xor (const uint8_t *in1, const uint8_t *in2, uint8_t *out, size_t len) +{ + size_t i; + + for (i = 0; i < len; ++i) + out[i] = in1[i] ^ in2[i]; +} + +static int +af_hash (gnutls_digest_algorithm_t hash_alg, uint8_t *block, size_t len) +{ + size_t digest_bytes = gnutls_hash_get_len (hash_alg); + size_t nr_blocks, last_block_len; + size_t i; + CLEANUP_FREE uint8_t *temp = malloc (digest_bytes); + int r; + gnutls_hash_hd_t hash; + + nr_blocks = len / digest_bytes; + last_block_len = len % digest_bytes; + if (last_block_len != 0) + nr_blocks++; + else + last_block_len = digest_bytes; + + for (i = 0; i < nr_blocks; ++i) { + const uint32_t iv = htobe32 (i); + const size_t blen = i < nr_blocks - 1 ? digest_bytes : last_block_len; + + /* Hash iv + i'th block into temp. */ + r = gnutls_hash_init (&hash, hash_alg); + if (r != 0) { + nbdkit_error ("gnutls_hash_init: %s", gnutls_strerror (r)); + return -1; + } + gnutls_hash (hash, &iv, sizeof iv); + gnutls_hash (hash, &block[i*digest_bytes], blen); + gnutls_hash_deinit (hash, temp); + + memcpy (&block[i*digest_bytes], temp, blen); + } + + return 0; +} + +static int +afmerge (gnutls_digest_algorithm_t hash_alg, uint32_t stripes, + const uint8_t *in, uint8_t *out, size_t outlen) +{ + CLEANUP_FREE uint8_t *block = calloc (1, outlen); + size_t i; + + /* NB: input size is stripes * master_key_len where + * master_key_len == outlen + */ + for (i = 0; i < stripes-1; ++i) { + xor (&in[i*outlen], block, block, outlen); + if (af_hash (hash_alg, block, outlen) == -1) + return -1; + } + xor (&in[i*outlen], block, out, outlen); + return 0; +} + +/* Length of key material in key slot i (sectors). + * + * This is basically copied from qemu because the spec description is + * unintelligible and apparently doesn't match reality. + */ +static uint64_t +key_material_length_in_sectors (struct handle *h, size_t i) +{ + uint64_t len, r; + + len = h->phdr.master_key_len * h->phdr.keyslot[i].stripes; + r = DIV_ROUND_UP (len, LUKS_SECTOR_SIZE); + r = ROUND_UP (r, LUKS_ALIGN_KEYSLOTS / LUKS_SECTOR_SIZE); + return r; +} + +/* Try the passphrase in key slot i. If this returns true then the + * passphrase was able to decrypt the master key, and the master key + * has been stored in h->masterkey. + */ +static int +try_passphrase_in_keyslot (nbdkit_next *next, struct handle *h, size_t i) +{ + struct luks_keyslot *ks = &h->phdr.keyslot[i]; + size_t split_key_len; + CLEANUP_FREE uint8_t *split_key = NULL; + CLEANUP_FREE uint8_t *masterkey = NULL; + const gnutls_datum_t key = + { (unsigned char *) passphrase, strlen (passphrase) }; + const gnutls_datum_t salt = + { (unsigned char *) ks->password_salt, LUKS_SALTSIZE }; + const gnutls_datum_t msalt = + { (unsigned char *) h->phdr.master_key_salt, LUKS_SALTSIZE }; + gnutls_datum_t mkey; + gnutls_cipher_hd_t cipher; + int r, err = 0; + uint64_t start; + uint8_t key_digest[LUKS_DIGESTSIZE]; + + if (ks->active != LUKS_KEY_ENABLED) + return 0; + + split_key_len = h->phdr.master_key_len * ks->stripes; + split_key = malloc (split_key_len); + if (split_key == NULL) { + nbdkit_error ("malloc: %m"); + return -1; + } + masterkey = malloc (h->phdr.master_key_len); + if (masterkey == NULL) { + nbdkit_error ("malloc: %m"); + return -1; + } + + /* Hash the passphrase to make a possible masterkey. */ + r = gnutls_pbkdf2 (h->hash_alg, &key, &salt, ks->password_iterations, + masterkey, h->phdr.master_key_len); + if (r != 0) { + nbdkit_error ("gnutls_pbkdf2: %s", gnutls_strerror (r)); + return -1; + } + + /* Read master key material from plugin. */ + start = ks->key_material_offset * LUKS_SECTOR_SIZE; + if (next->pread (next, split_key, split_key_len, start, 0, &err) == -1) { + errno = err; + return -1; + } + + /* Decrypt the (still AFsplit) master key material. */ + mkey.data = (unsigned char *) masterkey; + mkey.size = h->phdr.master_key_len; + r = gnutls_cipher_init (&cipher, h->gnutls_cipher, &mkey, NULL); + if (r != 0) { + nbdkit_error ("gnutls_cipher_init: %s", gnutls_strerror (r)); + return -1; + } + + r = do_decrypt (h, cipher, 0, split_key, split_key_len); + gnutls_cipher_deinit (cipher); + if (r == -1) + return -1; + + /* Decode AFsplit key to a possible masterkey. */ + if (afmerge (h->hash_alg, ks->stripes, split_key, + masterkey, h->phdr.master_key_len) == -1) + return -1; + + /* Check if the masterkey is correct by comparing hash of the + * masterkey with LUKS header. + */ + r = gnutls_pbkdf2 (h->hash_alg, &mkey, &msalt, + h->phdr.master_key_digest_iterations, + key_digest, LUKS_DIGESTSIZE); + if (r != 0) { + nbdkit_error ("gnutls_pbkdf2: %s", gnutls_strerror (r)); + return -1; + } + + if (memcmp (key_digest, h->phdr.master_key_digest, LUKS_DIGESTSIZE) == 0) { + /* The passphrase is correct so save the master key in the handle. */ + h->masterkey = malloc (h->phdr.master_key_len); + if (h->masterkey == NULL) { + nbdkit_error ("malloc: %m"); + return -1; + } + memcpy (h->masterkey, masterkey, h->phdr.master_key_len); + return 1; + } + + return 0; +} + +static int +luks_prepare (nbdkit_next *next, void *handle, int readonly) +{ + static const char expected_magic[] = LUKS_MAGIC; + struct handle *h = handle; + int64_t size; + int err = 0, r; + size_t i; + struct luks_keyslot *ks; + char uuid[41]; + + /* Check we haven't been called before, this should never happen. */ + assert (h->phdr.version == 0); + + /* Check the struct size matches the documentation. */ + assert (sizeof (struct luks_phdr) == 592); + + /* Check this is a LUKSv1 disk. */ + size = next->get_size (next); + if (size == -1) + return -1; + if (size < 16384) { + nbdkit_error ("disk is too small to be LUKS-encrypted"); + return -1; + } + + /* Read the phdr. */ + if (next->pread (next, &h->phdr, sizeof h->phdr, 0, 0, &err) == -1) { + errno = err; + return -1; + } + + if (memcmp (h->phdr.magic, expected_magic, LUKS_MAGIC_LEN) != 0) { + nbdkit_error ("this disk does not contain a LUKS header"); + return -1; + } + h->phdr.version = be16toh (h->phdr.version); + if (h->phdr.version != 1) { + nbdkit_error ("this disk contains a LUKS version %" PRIu16 " header, " + "but this filter only supports LUKSv1", + h->phdr.version); + return -1; + } + + /* Byte-swap the rest of the header. */ + h->phdr.payload_offset = be32toh (h->phdr.payload_offset); + h->phdr.master_key_len = be32toh (h->phdr.master_key_len); + h->phdr.master_key_digest_iterations = + be32toh (h->phdr.master_key_digest_iterations); + + for (i = 0; i < LUKS_NUMKEYS; ++i) { + ks = &h->phdr.keyslot[i]; + ks->active = be32toh (ks->active); + ks->password_iterations = be32toh (ks->password_iterations); + ks->key_material_offset = be32toh (ks->key_material_offset); + ks->stripes = be32toh (ks->stripes); + } + + /* Sanity check some fields. */ + if (h->phdr.payload_offset >= size / LUKS_SECTOR_SIZE) { + nbdkit_error ("bad LUKSv1 header: payload offset points beyond " + "the end of the disk"); + return -1; + } + + /* We derive several allocations from master_key_len so make sure + * it's not insane. + */ + if (h->phdr.master_key_len > 1024) { + nbdkit_error ("bad LUKSv1 header: master key is too long"); + return -1; + } + + for (i = 0; i < LUKS_NUMKEYS; ++i) { + uint64_t start, len; + + ks = &h->phdr.keyslot[i]; + switch (ks->active) { + case LUKS_KEY_ENABLED: + if (!ks->stripes) { + nbdkit_error ("bad LUKSv1 header: key slot %zu is corrupted", i); + return -1; + } + if (ks->stripes >= 10000) { + nbdkit_error ("bad LUKSv1 header: key slot %zu stripes too large", i); + return -1; + } + start = ks->key_material_offset; + len = key_material_length_in_sectors (h, i); + if (len > 4096) /* bound it at something reasonable */ { + nbdkit_error ("bad LUKSv1 header: key slot %zu key material length " + "is too large", i); + return -1; + } + if (start * LUKS_SECTOR_SIZE >= size || + (start + len) * LUKS_SECTOR_SIZE >= size) { + nbdkit_error ("bad LUKSv1 header: key slot %zu key material offset " + "points beyond the end of the disk", i); + return -1; + } + if (ks->password_iterations > ULONG_MAX) { + nbdkit_error ("bad LUKSv1 header: key slot %zu " + "iterations too large", i); + return -1; + } + /*FALLTHROUGH*/ + case LUKS_KEY_DISABLED: + break; + + default: + nbdkit_error ("bad LUKSv1 header: key slot %zu has " + "an invalid active flag", i); + return -1; + } + } + + /* Decode the ciphers. */ + if (parse_cipher_strings (h) == -1) + return -1; + + /* Dump some information about the header. */ + memcpy (uuid, h->phdr.uuid, 40); + uuid[40] = 0; + nbdkit_debug ("LUKS UUID: %s", uuid); + + for (i = 0; i < LUKS_NUMKEYS; ++i) { + uint64_t start, len; + + ks = &h->phdr.keyslot[i]; + if (ks->active == LUKS_KEY_ENABLED) { + start = ks->key_material_offset; + len = key_material_length_in_sectors (h, i); + nbdkit_debug ("LUKS key slot %zu: key material in sectors %" PRIu64 + "..%" PRIu64, + i, start, start+len-1); + } + } + + /* Now try to unlock the master key. */ + for (i = 0; i < LUKS_NUMKEYS; ++i) { + r = try_passphrase_in_keyslot (next, h, i); + if (r == -1) + return -1; + if (r > 0) + goto unlocked; + } + nbdkit_error ("LUKS passphrase is not correct, " + "no key slot could be unlocked"); + return -1; + + unlocked: + assert (h->masterkey != NULL); + nbdkit_debug ("LUKS unlocked block device with passphrase"); + + return 0; +} + +static int64_t +luks_get_size (nbdkit_next *next, void *handle) +{ + struct handle *h = handle; + int64_t size; + + /* Check that prepare has been called already. */ + assert (h->phdr.version > 0); + + size = next->get_size (next); + if (size == -1) + return -1; + + if (size < h->phdr.payload_offset * LUKS_SECTOR_SIZE) { + nbdkit_error ("disk too small, or contains an incomplete LUKS partition"); + return -1; + } + + size -= h->phdr.payload_offset * LUKS_SECTOR_SIZE; + return size; +} + +/* Whatever the plugin says, several operations are not supported by + * this filter: + * + * - extents + * - trim + * - zero + */ +static int +luks_can_extents (nbdkit_next *next, void *handle) +{ + return 0; +} + +static int +luks_can_trim (nbdkit_next *next, void *handle) +{ + return 0; +} + +static int +luks_can_zero (nbdkit_next *next, void *handle) +{ + return NBDKIT_ZERO_EMULATE; +} + +static int +luks_can_fast_zero (nbdkit_next *next, void *handle) +{ + return 0; +} + +/* Rely on nbdkit to call .pread to emulate .cache calls. We will + * respond by decrypting the block which could be stored by the cache + * filter or similar on top. + */ +static int +luks_can_cache (nbdkit_next *next, void *handle) +{ + return NBDKIT_CACHE_EMULATE; +} + +/* Advertise minimum/preferred sector-sized blocks, although we can in + * fact handle any read or write. + */ +static int +luks_block_size (nbdkit_next *next, void *handle, + uint32_t *minimum, uint32_t *preferred, uint32_t *maximum) +{ + if (next->block_size (next, minimum, preferred, maximum) == -1) + return -1; + + if (*minimum == 0) { /* No constraints set by the plugin. */ + *minimum = LUKS_SECTOR_SIZE; + *preferred = LUKS_SECTOR_SIZE; + *maximum = 0xffffffff; + } + else { + *minimum = MAX (*minimum, LUKS_SECTOR_SIZE); + *preferred = MAX (*minimum, MAX (*preferred, LUKS_SECTOR_SIZE)); + } + return 0; +} + +/* Decrypt data. */ +static int +luks_pread (nbdkit_next *next, void *handle, + void *buf, uint32_t count, uint64_t offset, + uint32_t flags, int *err) +{ + struct handle *h = handle; + const uint64_t payload_offset = h->phdr.payload_offset * LUKS_SECTOR_SIZE; + CLEANUP_FREE uint8_t *sector = NULL; + uint64_t sectnum, sectoffs; + const gnutls_datum_t mkey = + { (unsigned char *) h->masterkey, h->phdr.master_key_len }; + gnutls_cipher_hd_t cipher; + int r; + + if (!h->masterkey) { + *err = EIO; + return -1; + } + + if (!IS_ALIGNED (count | offset, LUKS_SECTOR_SIZE)) { + sector = malloc (LUKS_SECTOR_SIZE); + if (sector == NULL) { + *err = errno; + nbdkit_error ("malloc: %m"); + return -1; + } + } + + r = gnutls_cipher_init (&cipher, h->gnutls_cipher, &mkey, NULL); + if (r != 0) { + nbdkit_error ("gnutls_cipher_init: %s", gnutls_strerror (r)); + *err = EIO; + return -1; + } + + sectnum = offset / LUKS_SECTOR_SIZE; /* sector number */ + sectoffs = offset % LUKS_SECTOR_SIZE; /* offset within the sector */ + + /* Unaligned head */ + if (sectoffs) { + uint64_t n = MIN (LUKS_SECTOR_SIZE - sectoffs, count); + + assert (sector); + if (next->pread (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + if (do_decrypt (h, cipher, offset & ~LUKS_SECTOR_SIZE, + sector, LUKS_SECTOR_SIZE) == -1) + goto err; + + memcpy (buf, §or[sectoffs], n); + + buf += n; + count -= n; + offset += n; + sectnum++; + } + + /* Aligned body */ + while (count >= LUKS_SECTOR_SIZE) { + if (next->pread (next, buf, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + if (do_decrypt (h, cipher, offset, buf, LUKS_SECTOR_SIZE) == -1) + goto err; + + buf += LUKS_SECTOR_SIZE; + count -= LUKS_SECTOR_SIZE; + offset += LUKS_SECTOR_SIZE; + sectnum++; + } + + /* Unaligned tail */ + if (count) { + assert (sector); + if (next->pread (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + if (do_decrypt (h, cipher, offset, sector, LUKS_SECTOR_SIZE) == -1) + goto err; + + memcpy (buf, sector, count); + } + + gnutls_cipher_deinit (cipher); + return 0; + + err: + gnutls_cipher_deinit (cipher); + return -1; +} + +/* Lock preventing read-modify-write cycles from overlapping. */ +static pthread_mutex_t read_modify_write_lock = PTHREAD_MUTEX_INITIALIZER; + +/* Encrypt data. */ +static int +luks_pwrite (nbdkit_next *next, void *handle, + const void *buf, uint32_t count, uint64_t offset, + uint32_t flags, int *err) +{ + struct handle *h = handle; + const uint64_t payload_offset = h->phdr.payload_offset * LUKS_SECTOR_SIZE; + CLEANUP_FREE uint8_t *sector = NULL; + uint64_t sectnum, sectoffs; + const gnutls_datum_t mkey = + { (unsigned char *) h->masterkey, h->phdr.master_key_len }; + gnutls_cipher_hd_t cipher; + int r; + + if (!h->masterkey) { + *err = EIO; + return -1; + } + + sector = malloc (LUKS_SECTOR_SIZE); + if (sector == NULL) { + *err = errno; + nbdkit_error ("malloc: %m"); + return -1; + } + + r = gnutls_cipher_init (&cipher, h->gnutls_cipher, &mkey, NULL); + if (r != 0) { + nbdkit_error ("gnutls_cipher_init: %s", gnutls_strerror (r)); + *err = EIO; + return -1; + } + + sectnum = offset / LUKS_SECTOR_SIZE; /* sector number */ + sectoffs = offset % LUKS_SECTOR_SIZE; /* offset within the sector */ + + /* Unaligned head */ + if (sectoffs) { + ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&read_modify_write_lock); + + uint64_t n = MIN (LUKS_SECTOR_SIZE - sectoffs, count); + + if (next->pread (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + memcpy (§or[sectoffs], buf, n); + + if (do_encrypt (h, cipher, offset & ~LUKS_SECTOR_SIZE, + sector, LUKS_SECTOR_SIZE) == -1) + goto err; + + if (next->pwrite (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + buf += n; + count -= n; + offset += n; + sectnum++; + } + + /* Aligned body */ + while (count >= LUKS_SECTOR_SIZE) { + memcpy (sector, buf, LUKS_SECTOR_SIZE); + + if (do_encrypt (h, cipher, offset, sector, LUKS_SECTOR_SIZE) == -1) + goto err; + + if (next->pwrite (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + buf += LUKS_SECTOR_SIZE; + count -= LUKS_SECTOR_SIZE; + offset += LUKS_SECTOR_SIZE; + sectnum++; + } + + /* Unaligned tail */ + if (count) { + ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&read_modify_write_lock); + + if (next->pread (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + + memcpy (sector, buf, count); + + if (do_encrypt (h, cipher, offset, sector, LUKS_SECTOR_SIZE) == -1) + goto err; + + if (next->pwrite (next, sector, LUKS_SECTOR_SIZE, + sectnum * LUKS_SECTOR_SIZE + payload_offset, + flags, err) == -1) + goto err; + } + + gnutls_cipher_deinit (cipher); + return 0; + + err: + gnutls_cipher_deinit (cipher); + return -1; +} + +static struct nbdkit_filter filter = { + .name = "luks", + .longname = "nbdkit luks filter", + .unload = luks_unload, + .thread_model = luks_thread_model, + .config = luks_config, + .config_complete = luks_config_complete, + .config_help = luks_config_help, + .open = luks_open, + .close = luks_close, + .prepare = luks_prepare, + .get_size = luks_get_size, + .can_extents = luks_can_extents, + .can_trim = luks_can_trim, + .can_zero = luks_can_zero, + .can_fast_zero = luks_can_fast_zero, + .can_cache = luks_can_cache, + .block_size = luks_block_size, + .pread = luks_pread, + .pwrite = luks_pwrite, +}; + +NBDKIT_REGISTER_FILTER(filter) diff --git a/filters/luks/nbdkit-luks-filter.pod b/filters/luks/nbdkit-luks-filter.pod new file mode 100644 index 00000000..56e51561 --- /dev/null +++ b/filters/luks/nbdkit-luks-filter.pod @@ -0,0 +1,120 @@ +=head1 NAME + +nbdkit-luks-filter - read and write LUKS-encrypted disks and partitions + +=head1 SYNOPSIS + + nbdkit file encrypted-disk.img --filter=luks passphrase=+/tmp/secret + +=head1 DESCRIPTION + +C is a filter for L which transparently +opens a LUKS-encrypted disk image. LUKS ("Linux Unified Key Setup") +is the Full Disk Encryption (FDE) system commonly used by Linux +systems. This filter is compatible with LUKSv1 as implemented by the +Linux kernel (dm_crypt), and by qemu. + +You can place this filter on top of L to +decrypt a local file: + + nbdkit file encrypted-disk.img --filter=luks passphrase=+/tmp/secret + +If LUKS is present inside a partition in the disk image then you will +have to combine this filter with L. The +order of the filters is important: + + nbdkit file encrypted-disk.img \ + --filter=luks passphrase=+/tmp/secret \ + --filter=partition partition=1 + +This filter also works on top of other plugins such as +L: + + nbdkit curl https://example.com/encrypted-disk.img \ + --filter=luks passphrase=+/tmp/secret + +The web server sees only the encrypted data. Without knowing the +passphrase, the web server cannot access the decrypted disk. Only +encrypted data is sent over the HTTP connection. nbdkit itself will +serve I disk data over the NBD connection (if this is a +problem see L, or use a Unix domain socket I<-U>). + +The passphrase can be stored in a file (as shown), passed directly on +the command line (insecure), entered interactively, or passed to +nbdkit over a file descriptor. + +This filter can read and write LUKSv1. It cannot create disks, change +passphrases, add keyslots, etc. To do that, you can use ordinary +Linux tools like L. Note you must force LUKSv1 +(eg. using cryptsetup I<--type luks1>). L can also +create compatible disk images: + + qemu-img create -f luks \ + --object secret,data=SECRET,id=sec0 \ + -o key-secret=sec0 \ + encrypted-disk.img 1G + +=head1 PARAMETERS + +=over 4 + +=item BSECRET + +Use the secret passphrase when decrypting the disk. + +Note that passing this on the command line is not secure on shared +machines. + +=item B + +Ask for the passphrase (interactively) when nbdkit starts up. + +=item BFILENAME + +Read the passphrase from the named file. This is a secure method to +supply a passphrase, as long as you set the permissions on the file +appropriately. + +=item BFD + +Read the passphrase from file descriptor number C, inherited from +the parent process when nbdkit starts up. This is also a secure +method to supply a passphrase. + +=back + +=head1 FILES + +=over 4 + +=item F<$filterdir/nbdkit-luks-filter.so> + +The plugin. + +Use C to find the location of C<$filterdir>. + +=back + +=head1 VERSION + +C first appeared in nbdkit 1.32. + +=head1 SEE ALSO + +L, +L, +L, +L, +L, +L, +L, +L, +L. + +=head1 AUTHORS + +Richard W.M. Jones + +=head1 COPYRIGHT + +Copyright (C) 2013-2022 Red Hat Inc. diff --git a/plugins/file/nbdkit-file-plugin.pod b/plugins/file/nbdkit-file-plugin.pod index f8f0e198..b95e7349 100644 --- a/plugins/file/nbdkit-file-plugin.pod +++ b/plugins/file/nbdkit-file-plugin.pod @@ -223,6 +223,7 @@ L, L, L, L, +L, L. =head1 AUTHORS diff --git a/tests/Makefile.am b/tests/Makefile.am index b310e8a2..c29453ba 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1596,6 +1596,18 @@ EXTRA_DIST += \ test-log-script-info.sh \ $(NULL) +# luks filter test. +if HAVE_GNUTLS +TESTS += \ + test-luks-info.sh \ + test-luks-copy.sh \ + $(NULL) +endif +EXTRA_DIST += \ + test-luks-info.sh \ + test-luks-copy.sh \ + $(NULL) + # multi-conn filter test. TESTS += \ test-multi-conn.sh \ diff --git a/tests/test-luks-copy.sh b/tests/test-luks-copy.sh new file mode 100755 index 00000000..99f300d0 --- /dev/null +++ b/tests/test-luks-copy.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# nbdkit +# Copyright (C) 2018-2022 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Red Hat nor the names of its contributors may be +# used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +source ./functions.sh +set -e +set -x + +requires nbdcopy --version +requires nbdsh --version +requires_nbdsh_uri +requires qemu-img --version +requires bash -c 'qemu-img --help | grep -- --target-image-opts' +requires hexdump --version +requires truncate --version +requires_filter luks + +encrypt_disk=luks-copy1.img +plain_disk=luks-copy2.img +pid=luks-copy.pid +sock=$(mktemp -u /tmp/nbdkit-test-sock.XXXXXX) +cleanup_fn rm -f $encrypt_disk $plain_disk $pid $sock +rm -f $encrypt_disk $plain_disk $pid $sock + +# Create an empty encrypted disk container. +# +# NB: This is complicated because qemu doesn't create an all-zeroes +# plaintext disk for some reason when you use create -f luks. It +# starts with random plaintext. +# +# https://stackoverflow.com/a/44669936 +qemu-img create -f luks \ + --object secret,data=123456,id=sec0 \ + -o key-secret=sec0 \ + $encrypt_disk 10M +truncate -s 10M $plain_disk +qemu-img convert --target-image-opts -n \ + --object secret,data=123456,id=sec0 \ + $plain_disk \ + driver=luks,file.filename=$encrypt_disk,key-secret=sec0 +rm $plain_disk + +# Start nbdkit on the encrypted disk. +start_nbdkit -P $pid -U $sock \ + file $encrypt_disk --filter=luks passphrase=123456 +uri="nbd+unix:///?socket=$sock" + +# Copy the whole disk out. It should be empty. +nbdcopy "$uri" $plain_disk + +if [ "$(hexdump -C $plain_disk)" != '00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +* +00a00000' ]; then + echo "$0: expected plaintext disk to be empty" + exit 1 +fi + +# Use nbdsh to overwrite with some known data and check we can read +# back what we wrote. +nbdsh -u "$uri" \ + -c 'h.pwrite(b"1"*65536, 0)' \ + -c 'h.pwrite(b"2"*65536, 128*1024)' \ + -c 'h.pwrite(b"3"*65536, 9*1024*1024)' \ + -c 'buf = h.pread(65536, 0)' \ + -c 'assert buf == b"1"*65536' \ + -c 'buf = h.pread(65536, 65536)' \ + -c 'assert buf == bytearray(65536)' \ + -c 'buf = h.pread(65536, 128*1024)' \ + -c 'assert buf == b"2"*65536' \ + -c 'buf = h.pread(65536, 9*1024*1024)' \ + -c 'assert buf == b"3"*65536' \ + -c 'h.flush()' + +# Use qemu to copy out the whole disk. Note we called flush() above +# so the disk should be synchronised. +qemu-img convert --image-opts \ + --object secret,data=123456,id=sec0 \ + driver=luks,file.filename=$encrypt_disk,key-secret=sec0 \ + $plain_disk + +# Check the contents are expected. +if [ "$(hexdump -C $plain_disk)" != '00000000 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 |1111111111111111| +* +00010000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +* +00020000 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 |2222222222222222| +* +00030000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +* +00900000 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 |3333333333333333| +* +00910000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +* +00a00000' ]; then + echo "$0: unexpected content" + exit 1 +fi diff --git a/tests/test-luks-info.sh b/tests/test-luks-info.sh new file mode 100755 index 00000000..3eff657b --- /dev/null +++ b/tests/test-luks-info.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# nbdkit +# Copyright (C) 2018-2022 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Red Hat nor the names of its contributors may be +# used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +source ./functions.sh +set -e +set -x + +requires nbdinfo --version +requires qemu-img --version +requires_filter luks + +disk=luks-info.img +info=luks-info.log +cleanup_fn rm -f $disk $info +rm -f $disk $info + +qemu-img create -f luks \ + --object secret,data=123456,id=sec0 \ + -o key-secret=sec0 \ + $disk 10M + +nbdkit -U - file $disk --filter=luks passphrase=123456 \ + --run 'nbdinfo $uri' > $info +cat $info + +# Check the size is 10M (so it doesn't include the LUKS header). +grep "10485760" $info -- 2.31.1