Blob Blame History Raw
From c19936170cf8b385687cf40f5a9507d87ae08267 Mon Sep 17 00:00:00 2001
From: "Richard W.M. Jones" <rjones@redhat.com>
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<gnutls_priority_init(3)>.
 =head1 SEE ALSO
 
 L<nbdkit(1)>,
+L<nbdkit-luks-filter(1)>,
 L<nbdkit-tls-fallback-filter(1)>,
 L<nbdcopy(1)>,
 L<nbdfuse(1)>,
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 <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <limits.h>
+#include <assert.h>
+#include <pthread.h>
+
+#include <gnutls/crypto.h>
+
+#include <nbdkit-filter.h>
+
+#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>      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, &sector32, 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, &sector, 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, &sector[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 (&sector[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<nbdkit-luks-filter> is a filter for L<nbdkit(1)> 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<nbdkit-file-plugin(1)> 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<nbdkit-partition-filter(1)>.  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-plugin(1)>:
+
+ 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<unencrypted> disk data over the NBD connection (if this is a
+problem see L<nbdkit-tls(1)>, 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<cryptsetup(8)>.  Note you must force LUKSv1
+(eg. using cryptsetup I<--type luks1>).  L<qemu-img(1)> 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 B<passphrase=>SECRET
+
+Use the secret passphrase when decrypting the disk.
+
+Note that passing this on the command line is not secure on shared
+machines.
+
+=item B<passphrase=->
+
+Ask for the passphrase (interactively) when nbdkit starts up.
+
+=item B<passphrase=+>FILENAME
+
+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 B<passphrase=->FD
+
+Read the passphrase from file descriptor number C<FD>, 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<nbdkit --dump-config> to find the location of C<$filterdir>.
+
+=back
+
+=head1 VERSION
+
+C<nbdkit-luks-filter> first appeared in nbdkit 1.32.
+
+=head1 SEE ALSO
+
+L<nbdkit-curl-plugin(1)>,
+L<nbdkit-file-plugin(1)>,
+L<nbdkit-ip-filter(1)>,
+L<nbdkit-partition-filter(1)>,
+L<nbdkit(1)>,
+L<nbdkit-tls(1)>,
+L<nbdkit-plugin(3)>,
+L<cryptsetup(8)>,
+L<qemu-img(1)>.
+
+=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<nbdkit-partitioning-plugin(1)>,
 L<nbdkit-tmpdisk-plugin(1)>,
 L<nbdkit-exportname-filter(1)>,
 L<nbdkit-fua-filter(1)>,
+L<nbdkit-luks-filter(1)>,
 L<nbdkit-noextents-filter(1)>.
 
 =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