From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from list by lists.gnu.org with archive (Exim 4.90_1) id 1iVakw-0006rJ-Ul for mharc-grub-devel@gnu.org; Fri, 15 Nov 2019 07:31:18 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]:44951) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1iVaks-0006rB-M1 for grub-devel@gnu.org; Fri, 15 Nov 2019 07:31:17 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1iVakp-0002cc-V1 for grub-devel@gnu.org; Fri, 15 Nov 2019 07:31:14 -0500 Received: from dibed.net-space.pl ([84.10.22.86]:46817) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_3DES_EDE_CBC_SHA1:24) (Exim 4.71) (envelope-from ) id 1iVakp-0002cE-BI for grub-devel@gnu.org; Fri, 15 Nov 2019 07:31:11 -0500 Received: from router-fw.i.net-space.pl ([192.168.52.1]:41336 "EHLO tomti.i.net-space.pl") by router-fw-old.i.net-space.pl with ESMTP id S1844066AbfKOMbD (ORCPT ); Fri, 15 Nov 2019 13:31:03 +0100 X-Comment: RFC 2476 MSA function at dibed.net-space.pl logged sender identity as: dkiper Date: Fri, 15 Nov 2019 13:31:00 +0100 From: Daniel Kiper To: Patrick Steinhardt Cc: grub-devel@gnu.org, Max Tottenham Subject: Re: [PATCH v3 6/6] disk: Implement support for LUKS2 Message-ID: <20191115123100.dihbfkafa2qxkyfu@tomti.i.net-space.pl> References: <9d88fcbab11f94f04365e3636b1002542a55cb99.1573651222.git.ps@pks.im> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <9d88fcbab11f94f04365e3636b1002542a55cb99.1573651222.git.ps@pks.im> User-Agent: NeoMutt/20170113 (1.7.2) X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 84.10.22.86 X-BeenThere: grub-devel@gnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: The development of GNU GRUB List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 15 Nov 2019 12:31:17 -0000 On Wed, Nov 13, 2019 at 02:22:38PM +0100, Patrick Steinhardt wrote: > With cryptsetup 2.0, a new version of LUKS was introduced that breaks > compatibility with the previous version due to various reasons. GRUB > currently lacks any support for LUKS2, making it impossible to decrypt > disks encrypted with that version. This commit implements support for > this new format. > > Note that LUKS1 and LUKS2 are quite different data formats. While they > do share the same disk signature in the first few bytes, representation > of encryption parameters is completely different between both versions. > While the former version one relied on a single binary header, only, > LUKS2 uses the binary header only in order to locate the actual metadata > which is encoded in JSON. Furthermore, the new data format is a lot more > complex to allow for more flexible setups, like e.g. having multiple > encrypted segments and other features that weren't previously possible. > Because of this, it was decided that it doesn't make sense to keep both > LUKS1 and LUKS2 support in the same module and instead to implement it > in two different modules "luks" and "luks2". > > The proposed support for LUKS2 is able to make use of the metadata to > decrypt such disks. Note though that in the current version, only the > PBKDF2 key derival function is supported. This can mostly attributed to > the fact that the libgcrypt library currently has no support for either > Argon2i or Argon2id, which are the remaining KDFs supported by LUKS2. It > wouldn't have been much of a problem to bundle those algorithms with > GRUB itself, but it was decided against that in order to keep down the > number of patches required for initial LUKS2 support. Adding it in the > future would be trivial, given that the code structure is already in > place. > > Signed-off-by: Patrick Steinhardt > --- > Makefile.util.def | 4 +- > docs/grub.texi | 2 +- > grub-core/Makefile.core.def | 8 + > grub-core/disk/luks2.c | 672 ++++++++++++++++++++++++++++++++++++ > 4 files changed, 684 insertions(+), 2 deletions(-) > create mode 100644 grub-core/disk/luks2.c > > diff --git a/Makefile.util.def b/Makefile.util.def > index 969d32f00..94336392b 100644 > --- a/Makefile.util.def > +++ b/Makefile.util.def > @@ -3,7 +3,7 @@ AutoGen definitions Makefile.tpl; > library = { > name = libgrubkern.a; > cflags = '$(CFLAGS_GNULIB)'; > - cppflags = '$(CPPFLAGS_GNULIB)'; > + cppflags = '$(CPPFLAGS_GNULIB) -I$(srcdir)/grub-core/lib/json'; > > common = util/misc.c; > common = grub-core/kern/command.c; > @@ -36,7 +36,9 @@ library = { > common = grub-core/kern/misc.c; > common = grub-core/kern/partition.c; > common = grub-core/lib/crypto.c; > + common = grub-core/lib/json/json.c; > common = grub-core/disk/luks.c; > + common = grub-core/disk/luks2.c; > common = grub-core/disk/geli.c; > common = grub-core/disk/cryptodisk.c; > common = grub-core/disk/AFSplitter.c; > diff --git a/docs/grub.texi b/docs/grub.texi > index c25ab7a5f..ee28fd7e1 100644 > --- a/docs/grub.texi > +++ b/docs/grub.texi > @@ -4211,7 +4211,7 @@ is requested interactively. Option @var{device} configures specific grub device > with specified @var{uuid}; option @option{-a} configures all detected encrypted > devices; option @option{-b} configures all geli containers that have boot flag set. > > -GRUB suports devices encrypted using LUKS and geli. Note that necessary modules (@var{luks} and @var{geli}) have to be loaded manually before this command can > +GRUB suports devices encrypted using LUKS and geli. Note that necessary modules (@var{luks}, @var{luks2} and @var{geli}) have to be loaded manually before this command can s/LUKS/LUKS, LUKS2/ > be used. > @end deffn > > diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def > index db346a9f4..a0507a1fa 100644 > --- a/grub-core/Makefile.core.def > +++ b/grub-core/Makefile.core.def > @@ -1191,6 +1191,14 @@ module = { > common = disk/luks.c; > }; > > +module = { > + name = luks2; > + common = disk/luks2.c; > + common = lib/gnulib/base64.c; > + cflags = '$(CFLAGS_POSIX) $(CFLAGS_GNULIB)'; > + cppflags = '$(CPPFLAGS_POSIX) $(CPPFLAGS_GNULIB) -I$(srcdir)/lib/json'; > +}; > + > module = { > name = geli; > common = disk/geli.c; > diff --git a/grub-core/disk/luks2.c b/grub-core/disk/luks2.c > new file mode 100644 > index 000000000..f66e75de3 > --- /dev/null > +++ b/grub-core/disk/luks2.c > @@ -0,0 +1,672 @@ > +/* > + * GRUB -- GRand Unified Bootloader > + * Copyright (C) 2019 Free Software Foundation, Inc. > + * > + * GRUB is free software: you can redistribute it and/or modify > + * it under the terms of the GNU General Public License as published by > + * the Free Software Foundation, either version 3 of the License, or > + * (at your option) any later version. > + * > + * GRUB is distributed in the hope that it will be useful, > + * but WITHOUT ANY WARRANTY; without even the implied warranty of > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > + * GNU General Public License for more details. > + * > + * You should have received a copy of the GNU General Public License > + * along with GRUB. If not, see . > + */ > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#include > +#include > + > +#define MAX_PASSPHRASE 256 Move this constant below GRUB_MOD_LICENSE(). > +GRUB_MOD_LICENSE ("GPLv3+"); > + > +gcry_err_code_t AF_merge (const gcry_md_spec_t * hash, grub_uint8_t * src, > + grub_uint8_t * dst, grub_size_t blocksize, > + grub_size_t blocknumbers); Please move this behind types definitions and before first function. > +enum grub_luks2_kdf_type > +{ > + LUKS2_KDF_TYPE_ARGON2I, > + LUKS2_KDF_TYPE_PBKDF2 > +}; > +typedef enum grub_luks2_kdf_type grub_luks2_kdf_type_t; > + > +/* On disk LUKS header */ > +struct grub_luks2_header > +{ > + char magic[6]; > +#define LUKS_MAGIC_1ST "LUKS\xBA\xBE" > +#define LUKS_MAGIC_2ND "SKUL\xBA\xBE" Please move this constants outside of the struct and put them before MAX_PASSPHRASE. > + grub_uint16_t version; > + grub_uint64_t hdr_size; > + grub_uint64_t seqid; > + char label[48]; > + char csum_alg[32]; > + grub_uint8_t salt[64]; > + char uuid[40]; > + char subsystem[48]; > + grub_uint64_t hdr_offset; > + char _padding[184]; > + grub_uint8_t csum[64]; > + char _padding4096[7*512]; Please align all member names in one column. > +} GRUB_PACKED; > +typedef struct grub_luks2_header grub_luks2_header_t; > + > +struct grub_luks2_keyslot > +{ > + grub_int64_t key_size; > + grub_int64_t priority; > + struct > + { > + const char *encryption; > + grub_uint64_t offset; > + grub_uint64_t size; > + grub_int64_t key_size; > + } area; > + struct > + { > + const char *hash; > + grub_int64_t stripes; > + } af; > + struct > + { > + grub_luks2_kdf_type_t type; > + const char *salt; > + union > + { > + struct > + { > + grub_int64_t time; > + grub_int64_t memory; > + grub_int64_t cpus; > + } argon2i; > + struct > + { > + const char *hash; > + grub_int64_t iterations; > + } pbkdf2; > + } u; > + } kdf; Ditto... > +}; > +typedef struct grub_luks2_keyslot grub_luks2_keyslot_t; > + > +struct grub_luks2_segment > +{ > + grub_uint64_t offset; > + const char *size; > + const char *encryption; > + grub_int64_t sector_size; Ditto and below... E.g. grub_uint64_t offset const char *size; const char *encryption; grub_int64_t sector_size; > +}; > +typedef struct grub_luks2_segment grub_luks2_segment_t; > + > +struct grub_luks2_digest > +{ > + /* Both keyslots and segments are interpreted as bitfields here */ > + grub_uint64_t keyslots; > + grub_uint64_t segments; > + const char *salt; > + const char *digest; > + const char *hash; > + grub_int64_t iterations; > +}; > +typedef struct grub_luks2_digest grub_luks2_digest_t; > + [...] > +/* Determine whether to use primary or secondary header */ > +static grub_err_t > +luks2_read_header (grub_disk_t disk, grub_luks2_header_t *outhdr) > +{ > + grub_luks2_header_t primary, secondary, *header = &primary; > + grub_err_t err; > + > + /* Read the primary LUKS header. */ > + err = grub_disk_read (disk, 0, 0, sizeof (primary), &primary); > + if (err) > + return err; > + > + /* Look for LUKS magic sequence. */ > + if (grub_memcmp (primary.magic, LUKS_MAGIC_1ST, sizeof (primary.magic)) > + || grub_be_to_cpu16 (primary.version) != 2) Please be consistent and put "||" at the end of first line as you did earlier. > + return GRUB_ERR_BAD_SIGNATURE; > + > + /* Read the secondary header. */ > + err = grub_disk_read (disk, 0, grub_be_to_cpu64 (primary.hdr_size), sizeof (secondary), &secondary); > + if (err) > + return err; > + > + /* Look for LUKS magic sequence. */ > + if (grub_memcmp (secondary.magic, LUKS_MAGIC_2ND, sizeof (secondary.magic)) > + || grub_be_to_cpu16 (secondary.version) != 2) Ditto. > + return GRUB_ERR_BAD_SIGNATURE; > + > + if (grub_be_to_cpu64 (primary.seqid) < grub_be_to_cpu64 (secondary.seqid)) > + header = &secondary; > + grub_memcpy (outhdr, header, sizeof (*header)); > + > + return GRUB_ERR_NONE; > +} > + > +static grub_cryptodisk_t > +luks2_scan (grub_disk_t disk, const char *check_uuid, int check_boot) > +{ > + grub_cryptodisk_t cryptodisk; > + grub_luks2_header_t header; > + grub_err_t err; > + > + if (check_boot) > + return NULL; > + > + err = luks2_read_header (disk, &header); > + if (err) You can check directly for luks2_read_header() result and drop err. > + { > + grub_errno = GRUB_ERR_NONE; > + return NULL; > + } > + > + if (check_uuid && grub_strcasecmp (check_uuid, header.uuid) != 0) > + return NULL; > + > + cryptodisk = grub_zalloc (sizeof (*cryptodisk)); > + if (!cryptodisk) > + return NULL; Please add empty line here. > + COMPILE_TIME_ASSERT (sizeof (cryptodisk->uuid) >= sizeof (header.uuid)); Ditto... > + grub_memcpy (cryptodisk->uuid, header.uuid, sizeof (header.uuid)); > + cryptodisk->modname = "luks2"; > + return cryptodisk; > +} > + > +static grub_err_t > +luks2_verify_key (grub_luks2_digest_t *d, grub_uint8_t *candidate_key, > + grub_size_t candidate_key_len) > +{ > + grub_uint8_t candidate_digest[GRUB_CRYPTODISK_MAX_KEYLEN]; > + grub_uint8_t digest[GRUB_CRYPTODISK_MAX_KEYLEN], salt[GRUB_CRYPTODISK_MAX_KEYLEN]; > + grub_size_t saltlen = sizeof (salt), digestlen = sizeof (digest); > + const gcry_md_spec_t *hash; > + gcry_err_code_t gcry_err; > + > + /* Decdoe both digest and salt */ > + if (!base64_decode(d->digest, grub_strlen (d->digest), (char *)digest, &digestlen)) > + return grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid digest"); Formatting issue with "return...". > + if (!base64_decode(d->salt, grub_strlen (d->salt), (char *)salt, &saltlen)) > + return grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid digest salt"); Ditto and below... > + /* Configure the hash used for the digest. */ > + hash = grub_crypto_lookup_md_by_name (d->hash); > + if (!hash) > + return grub_error (GRUB_ERR_FILE_NOT_FOUND, "Couldn't load %s hash", d->hash); > + > + /* Calculate the candidate key's digest */ > + gcry_err = grub_crypto_pbkdf2 (hash, > + candidate_key, candidate_key_len, > + salt, saltlen, > + d->iterations, > + candidate_digest, digestlen); > + if (gcry_err) > + return grub_crypto_gcry_error (gcry_err); > + > + if (grub_memcmp (candidate_digest, digest, digestlen) != 0) > + return grub_error (GRUB_ERR_ACCESS_DENIED, "Mismatching digests"); > + > + return GRUB_ERR_NONE; > +} > + > +static grub_err_t > +luks2_decrypt_key (grub_uint8_t *out_key, > + grub_disk_t disk, grub_cryptodisk_t crypt, > + grub_luks2_keyslot_t *k, > + const grub_uint8_t *passphrase, grub_size_t passphraselen) > +{ > + grub_uint8_t area_key[GRUB_CRYPTODISK_MAX_KEYLEN]; > + grub_uint8_t salt[GRUB_CRYPTODISK_MAX_KEYLEN]; > + grub_uint8_t *split_key = NULL; > + grub_size_t saltlen = sizeof (salt); > + char cipher[32], *p;; > + const gcry_md_spec_t *hash; > + gcry_err_code_t gcry_err; s/gcry_err/gcry_ret/ > + grub_err_t err; s/err/ret/ Variable name conflicts with label name. Please do not do that even if compiler does not complain. And please do both changes in all functions. > + if (!base64_decode(k->kdf.salt, grub_strlen (k->kdf.salt), > + (char *)salt, &saltlen)) > + { > + err = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid keyslot salt"); > + goto err; > + } > + > + /* Calculate the binary area key of the user supplied passphrase. */ > + switch (k->kdf.type) > + { > + case LUKS2_KDF_TYPE_ARGON2I: > + err = grub_error (GRUB_ERR_BAD_ARGUMENT, "Argon2 not supported"); > + goto err; > + case LUKS2_KDF_TYPE_PBKDF2: > + hash = grub_crypto_lookup_md_by_name (k->kdf.u.pbkdf2.hash); > + if (!hash) > + { > + err = grub_error (GRUB_ERR_FILE_NOT_FOUND, "Couldn't load %s hash", > + k->kdf.u.pbkdf2.hash); > + goto err; > + } > + > + gcry_err = grub_crypto_pbkdf2 (hash, (grub_uint8_t *) passphrase, > + passphraselen, > + salt, saltlen, > + k->kdf.u.pbkdf2.iterations, > + area_key, k->area.key_size); > + if (gcry_err) > + { > + err = grub_crypto_gcry_error (gcry_err); > + goto err; > + } > + > + break; > + } > + > + /* Set up disk encryption parameters for the key area */ > + grub_strncpy (cipher, k->area.encryption, sizeof(cipher)); > + p = grub_memchr (cipher, '-', grub_strlen (cipher)); > + if (!p) > + return grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid encryption"); > + *p = '\0'; > + > + err = grub_cryptodisk_setcipher (crypt, cipher, p + 1); > + if (err) > + return err; > + > + gcry_err = grub_cryptodisk_setkey (crypt, area_key, k->area.key_size); > + if (gcry_err) > + { > + err = grub_crypto_gcry_error (gcry_err); > + goto err; > + } > + > + /* Read and decrypt the binary key area with the area key. */ > + split_key = grub_malloc (k->area.size); > + if (!split_key) > + { > + err = grub_errno; > + goto err; > + } > + > + grub_errno = GRUB_ERR_NONE; > + err = grub_disk_read (disk, 0, k->area.offset, k->area.size, split_key); > + if (err) > + { > + grub_dprintf ("luks2", "Read error: %s\n", grub_errmsg); > + goto err; > + } > + > + gcry_err = grub_cryptodisk_decrypt (crypt, split_key, k->area.size, 0); > + if (gcry_err) > + { > + err = grub_crypto_gcry_error (gcry_err); > + goto err; > + } > + > + /* Configure the hash used for anti-forensic merging. */ > + hash = grub_crypto_lookup_md_by_name (k->af.hash); > + if (!hash) > + { > + err = grub_error (GRUB_ERR_FILE_NOT_FOUND, "Couldn't load %s hash", > + k->af.hash); > + goto err; > + } > + > + /* Merge the decrypted key material to get the candidate master key. */ > + gcry_err = AF_merge (hash, split_key, out_key, k->key_size, k->af.stripes); > + if (gcry_err) > + { > + err = grub_crypto_gcry_error (gcry_err); > + goto err; > + } > + > + grub_dprintf ("luks2", "Candidate key recovered\n"); > + > +err: One space before label please. > + grub_free (split_key); > + return err; > +} > + > +static grub_err_t > +luks2_recover_key (grub_disk_t disk, > + grub_cryptodisk_t crypt) > +{ > + grub_uint8_t candidate_key[GRUB_CRYPTODISK_MAX_KEYLEN]; > + char passphrase[MAX_PASSPHRASE], cipher[32]; > + char *json_header = NULL, *part = NULL, *ptr; > + grub_size_t candidate_key_len = 0, i; > + grub_luks2_header_t header; > + grub_luks2_keyslot_t keyslot; > + grub_luks2_digest_t digest; > + grub_luks2_segment_t segment; > + gcry_err_code_t gcry_err; Ditto... > + grub_json_t *json = NULL, keyslots; > + grub_err_t err; Ditto... > + > + err = luks2_read_header (disk, &header); > + if (err) > + return err; > + > + json_header = grub_zalloc (grub_be_to_cpu64 (header.hdr_size) - sizeof (header)); > + if (!json_header) > + return GRUB_ERR_OUT_OF_MEMORY; > + > + /* Read the JSON area. */ > + err = grub_disk_read (disk, 0, grub_be_to_cpu64 (header.hdr_offset) + sizeof (header), > + grub_be_to_cpu64 (header.hdr_size) - sizeof (header), json_header); > + if (err) > + goto err; > + > + ptr = grub_memchr(json_header, 0, grub_be_to_cpu64 (header.hdr_size) - sizeof (header)); > + if (!ptr) > + goto err; > + > + err = grub_json_parse (&json, json_header, grub_be_to_cpu64 (header.hdr_size)); > + if (err) > + { > + err = grub_error (GRUB_ERR_BAD_ARGUMENT, "Invalid LUKS2 JSON header"); > + goto err; > + } > + > + /* Get the passphrase from the user. */ > + if (disk->partition) > + part = grub_partition_get_name (disk->partition); > + grub_printf_ (N_("Enter passphrase for %s%s%s (%s): "), disk->name, > + disk->partition ? "," : "", part ? : "", > + crypt->uuid); > + if (!grub_password_get (passphrase, MAX_PASSPHRASE)) > + { > + err = grub_error (GRUB_ERR_BAD_ARGUMENT, "Passphrase not supplied"); > + goto err; > + } > + > + err = grub_json_getvalue (&keyslots, json, "keyslots"); > + if (err) > + goto err; > + > + /* Try all keyslot */ > + for (i = 0; i < grub_json_getsize (&keyslots); i++) > + { > + err = luks2_get_keyslot (&keyslot, &digest, &segment, json, i); > + if (err) > + goto err; > + > + if (keyslot.priority == 0) > + { > + grub_dprintf ("luks2", "Ignoring keyslot %"PRIuGRUB_SIZE" due to priority\n", i); > + continue; > + } > + > + grub_dprintf ("luks2", "Trying keyslot %"PRIuGRUB_SIZE"\n", i); > + > + /* Set up disk according to keyslot's segment. */ > + crypt->offset = segment.offset / segment.sector_size; > + crypt->log_sector_size = sizeof (unsigned int) * 8 > + - __builtin_clz ((unsigned int) segment.sector_size) - 1; > + if (grub_strcmp (segment.size, "dynamic") == 0) > + crypt->total_length = grub_disk_get_size (disk) - crypt->offset; > + else > + crypt->total_length = grub_strtoull(segment.size, NULL, 10); > + > + err = luks2_decrypt_key (candidate_key, disk, crypt, &keyslot, > + (const grub_uint8_t *) passphrase, grub_strlen (passphrase)); > + if (err) > + { > + grub_dprintf ("luks2", "Decryption with keyslot %"PRIuGRUB_SIZE" failed", i); > + continue; > + } > + > + err = luks2_verify_key (&digest, candidate_key, keyslot.key_size); > + if (err) > + { > + grub_dprintf ("luks2", "Could not open keyslot %"PRIuGRUB_SIZE"\n", i); > + continue; > + } > + > + /* TRANSLATORS: It's a cryptographic key slot: one element of an array > + where each element is either empty or holds a key. */ Incorrectly formatted comment... Daniel