]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homed/fscrypt: add new xattr format hardening key sealing 41816/head
authorLuca Boccassi <luca.boccassi@gmail.com>
Fri, 24 Apr 2026 19:43:30 +0000 (20:43 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Tue, 19 May 2026 12:35:19 +0000 (13:35 +0100)
The current key sealing format has some less-than-ideal weaknesses:

- PBKDF2 with only 65k iterations, where recommendations are ~200k
- AES with null IV, relying on salt for uniqueness
- lack of AES MAC/AEAD

However improbable, it is at least theorically possible that with
a lot of resources an offline bruteforce could be attempted.

Add a v2 sealing format, keeping unsealing compatibility with
the current format:

 v2:<iterations>:<salt>:<IV>:<ciphertext>:<aes tag>

and use 600k iterations for the PBKDF2 sha512

docs/HOME_DIRECTORY.md
src/home/homework-fscrypt.c
test/units/TEST-46-HOMED.sh

index 57d87f5eeac0838078ad570557f26d971e6459ee..0ef39e9737254f619a65bac7ccf81ca6f38199fb 100644 (file)
@@ -54,11 +54,15 @@ mechanism, except that the home directory is encrypted using `fscrypt`.
 Key management is implemented via extended attributes on the directory itself:
 for each password an extended attribute `trusted.fscrypt_slot0`, `trusted.fscrypt_slot1`,
 `trusted.fscrypt_slot2`, … is maintained.
-Its value contains a colon-separated pair of Base64 encoded data fields.
-The first field contains a salt value, the second field the encrypted volume key.
-The latter is encrypted using AES256 in counter mode, using a key derived from the password via PBKDF2-HMAC-SHA512,
-together with the salt value.
-The construction is similar to what LUKS does for`dm-crypt` encrypted volumes.
+New slots are written in the `v2` format: a colon-separated string of the form
+`$v2:<iterations>:<salt>:<iv>:<ciphertext>:<tag>`, where `<iterations>` is a decimal integer
+and the salt, IV, ciphertext and tag fields are Base64 encoded.
+The volume key is wrapped with AES-256-GCM (authenticated encryption with a random IV)
+under a key derived from the password via PBKDF2-HMAC-SHA512 with the stored iteration count.
+For backward compatibility, legacy slots written by older versions of systemd-homed
+(a colon-separated `<salt>:<ciphertext>` pair encrypted with AES-256-CTR and a 0xFFFF-iteration
+PBKDF2-HMAC-SHA512 KDF) continue to be readable, and they are upgraded to the v2 format the next
+time the password is changed.
 Note that extended attributes are not encrypted by `fscrypt` and hence are suitable for carrying the key slots.
 Moreover, by using extended attributes, the slots are directly attached to the directory and
 an independent sidecar key database is not required.
index 6f8ae4b8c9c1c308fc9529151502712c722e3017..432f5d8678a88af3653b733b03480abaa3411b0e 100644 (file)
@@ -10,6 +10,7 @@
 #include "alloc-util.h"
 #include "crypto-util.h"
 #include "errno-util.h"
+#include "extract-word.h"
 #include "fd-util.h"
 #include "format-util.h"
 #include "hexdecoct.h"
@@ -18,6 +19,7 @@
 #include "homework-password-cache.h"
 #include "homework-quota.h"
 #include "homework.h"
+#include "iovec-util.h"
 #include "keyring-util.h"
 #include "log.h"
 #include "memory-util.h"
@@ -186,12 +188,33 @@ static void calculate_key_descriptor(
         memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE);
 }
 
-static int fscrypt_slot_try_one(
+/* fscrypt slot wrapping
+ *
+ * Two on-disk formats are supported. New slots are always written in v2, which improves offline security.
+ *
+ *   v1 (legacy, read-only):
+ *      <salt_b64>:<ciphertext_b64>
+ *      KDF: PBKDF2-HMAC-SHA512, 0xFFFF iterations
+ *      Cipher: AES-256-CTR, all-zero IV (relies on per-slot random salt for key uniqueness)
+ *      Integrity: 64-bit truncated double-SHA512 key descriptor comparison only
+ *
+ *   v2:
+ *      $v2:<iterations_dec>:<salt_b64>:<iv_b64>:<ciphertext_b64>:<tag_b64>
+ *      KDF: PBKDF2-HMAC-SHA512, FSCRYPT_SLOT_PBKDF2_ITERATIONS iterations (cost stored per slot)
+ *      Cipher: AES-256-GCM with explicit random 96-bit IV and 128-bit authentication tag
+ */
+
+#define FSCRYPT_SLOT_PBKDF2_ITERATIONS UINT32_C(600000)
+#define FSCRYPT_SLOT_SALT_SIZE 64u
+#define FSCRYPT_SLOT_GCM_IV_SIZE 12u
+#define FSCRYPT_SLOT_GCM_TAG_SIZE 16u
+
+static int fscrypt_slot_try_v1(
                 const char *password,
                 const void *salt, size_t salt_size,
                 const void *encrypted, size_t encrypted_size,
                 const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
-                void **ret_decrypted, size_t *ret_decrypted_size) {
+                struct iovec *ret_decrypted) {
 
         _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
         _cleanup_(erase_and_freep) void *decrypted = NULL;
@@ -273,25 +296,228 @@ static int fscrypt_slot_try_one(
         if (r < 0)
                 return r;
 
+        if (ret_decrypted) {
+                ret_decrypted->iov_base = TAKE_PTR(decrypted);
+                ret_decrypted->iov_len = decrypted_size;
+        }
+
+        return 0;
+}
+
+static int fscrypt_slot_try_v2(
+                const char *password,
+                uint32_t iterations,
+                const struct iovec *salt,
+                const struct iovec *iv,
+                const struct iovec *encrypted,
+                const struct iovec *tag,
+                const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+                struct iovec *ret_decrypted) {
+
+        _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+        _cleanup_(erase_and_freep) void *decrypted = NULL;
+        uint8_t key_descriptor[FS_KEY_DESCRIPTOR_SIZE];
+        int decrypted_size_out1 = 0, decrypted_size_out2 = 0;
+        uint8_t derived[512 / 8] = {};
+        size_t decrypted_size;
+        const EVP_CIPHER *cc;
+        int r;
+
+        assert(password);
+        assert(iterations > 0);
+        assert(iovec_is_set(salt));
+        assert(iovec_is_set(iv));
+        assert(iovec_is_set(encrypted));
+        assert(iovec_is_set(tag));
+        assert(match_key_descriptor);
+
+        r = dlopen_libcrypto(LOG_ERR);
+        if (r < 0)
+                return r;
+
+        CLEANUP_ERASE(derived);
+
+        if (sym_PKCS5_PBKDF2_HMAC(
+                            password, strlen(password),
+                            salt->iov_base, salt->iov_len,
+                            (int) iterations, sym_EVP_sha512(),
+                            sizeof(derived), derived) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed.");
+
+        context = sym_EVP_CIPHER_CTX_new();
+        if (!context)
+                return log_oom();
+
+        assert_se(cc = sym_EVP_aes_256_gcm());
+
+        /* We only use the first 256 bit of the derived key */
+        assert(sizeof(derived) >= (size_t) sym_EVP_CIPHER_get_key_length(cc));
+
+        if (sym_EVP_DecryptInit_ex(context, cc, /* impl= */ NULL, /* key= */ NULL, /* iv= */ NULL) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context.");
+
+        if (sym_EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, (int) iv->iov_len, /* ptr= */ NULL) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set GCM IV length.");
+
+        if (sym_EVP_DecryptInit_ex(context, /* type= */ NULL, /* impl= */ NULL, derived, iv->iov_base) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set decryption key/IV.");
+
+        if (__builtin_add_overflow(encrypted->iov_len, (size_t) sym_EVP_CIPHER_get_block_size(cc), &decrypted_size))
+                return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Decrypted buffer size would overflow.");
+
+        decrypted = malloc(decrypted_size);
+        if (!decrypted)
+                return log_oom();
+
+        if (sym_EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted->iov_base, encrypted->iov_len) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt volume key.");
+
+        assert((size_t) decrypted_size_out1 <= decrypted_size);
+
+        /* Set the expected GCM tag before finalisation, as an authentication failure here means the wrong
+         * password (or a tampered slot). */
+        if (sym_EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_TAG, (int) tag->iov_len, tag->iov_base) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set GCM tag.");
+
+        if (sym_EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1)
+                return -ENOANO; /* GCM authentication failed: wrong password or tampered slot */
+
+        assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 <= decrypted_size);
+        decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2;
+
+        calculate_key_descriptor(decrypted, decrypted_size, key_descriptor);
+
+        if (memcmp(key_descriptor, match_key_descriptor, FS_KEY_DESCRIPTOR_SIZE) != 0)
+                /* Authenticated decryption succeeded but the resulting volume key does not match the policy
+                 * descriptor. Treat as a non-match (e.g. leftover slot from a different fscrypt setup). */
+                return -ENOANO;
+
+        r = fscrypt_upload_volume_key(key_descriptor, decrypted, decrypted_size, KEY_SPEC_THREAD_KEYRING);
+        if (r < 0)
+                return r;
+
+        if (ret_decrypted) {
+                ret_decrypted->iov_base = TAKE_PTR(decrypted);
+                ret_decrypted->iov_len = decrypted_size;
+        }
+
+        return 0;
+}
+
+static int fscrypt_slot_try_one(
+                const char *password,
+                const char *xattr_value, size_t xattr_size,
+                const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+                struct iovec *ret_decrypted) {
+
+        _cleanup_free_ void *salt = NULL, *iv = NULL, *encrypted = NULL, *tag = NULL;
+        size_t salt_size, iv_size, encrypted_size, tag_size;
+        const char *p, *e;
+        const void *body;
+        int r;
+
+        assert(password);
+        assert(xattr_value);
+        assert(xattr_size > 0);
+        assert(match_key_descriptor);
+
+        body = memory_startswith(xattr_value, xattr_size, "$v2:");
+        if (!body) {
+                /* Legacy v1 format: "<salt_b64>:<ciphertext_b64>" */
+                log_debug("fscrypt slot uses legacy v1 format, will upgrade to v2 on next password change.");
+
+                e = memchr(xattr_value, ':', xattr_size);
+                if (!e)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed legacy fscrypt slot (no separator).");
+
+                r = unbase64mem_full(xattr_value, e - xattr_value, /* secure= */ false, &salt, &salt_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to decode legacy salt: %m");
+
+                r = unbase64mem_full(e + 1, xattr_size - (e - xattr_value) - 1, /* secure= */ false, &encrypted, &encrypted_size);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to decode legacy ciphertext: %m");
+
+                return fscrypt_slot_try_v1(password,
+                                           salt, salt_size,
+                                           encrypted, encrypted_size,
+                                           match_key_descriptor,
+                                           ret_decrypted);
+        }
+
+        /* v2 format: "$v2:<iterations>:<salt_b64>:<iv_b64>:<ct_b64>:<tag_b64>". Reject if it has NULs. */
+        _cleanup_free_ char *body_str = NULL;
+        r = make_cstring(body, xattr_size - STRLEN("$v2:"), MAKE_CSTRING_REFUSE_TRAILING_NUL, &body_str);
+        if (r < 0)
+                return log_error_errno(r, "Malformed v2 fscrypt slot: %m");
+
+        _cleanup_free_ char *iter_str = NULL, *salt_b64 = NULL, *iv_b64 = NULL,
+                            *encrypted_b64 = NULL, *tag_b64 = NULL;
+        uint32_t iterations;
+
+        p = body_str;
+        r = extract_many_words(&p, ":", EXTRACT_DONT_COALESCE_SEPARATORS | EXTRACT_RETAIN_ESCAPE,
+                               &iter_str, &salt_b64, &iv_b64, &encrypted_b64, &tag_b64);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse v2 fscrypt slot: %m");
+        if (r < 5)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed v2 fscrypt slot.");
+
+        if (safe_atou32(iter_str, &iterations) < 0 || iterations == 0 || iterations > INT_MAX)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid iteration count in v2 fscrypt slot.");
+
+        r = unbase64mem(salt_b64, &salt, &salt_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode v2 salt: %m");
+        if (salt_size == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid v2 salt size.");
+
+        r = unbase64mem(iv_b64, &iv, &iv_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode v2 IV: %m");
+        if (iv_size != FSCRYPT_SLOT_GCM_IV_SIZE)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid v2 IV size.");
+
+        r = unbase64mem(encrypted_b64, &encrypted, &encrypted_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode v2 ciphertext: %m");
+        if (encrypted_size == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty v2 ciphertext.");
+
+        r = unbase64mem(tag_b64, &tag, &tag_size);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode v2 tag: %m");
+        if (tag_size != FSCRYPT_SLOT_GCM_TAG_SIZE)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid v2 tag size.");
+
+        _cleanup_(iovec_done_erase) struct iovec decrypted = {};
+        r = fscrypt_slot_try_v2(password,
+                                iterations,
+                                &IOVEC_MAKE(salt, salt_size),
+                                &IOVEC_MAKE(iv, iv_size),
+                                &IOVEC_MAKE(encrypted, encrypted_size),
+                                &IOVEC_MAKE(tag, tag_size),
+                                match_key_descriptor,
+                                &decrypted);
+        if (r < 0)
+                return r;
+
         if (ret_decrypted)
-                *ret_decrypted = TAKE_PTR(decrypted);
-        if (ret_decrypted_size)
-                *ret_decrypted_size = decrypted_size;
+                *ret_decrypted = TAKE_STRUCT(decrypted);
 
         return 0;
 }
 
 static int fscrypt_slot_try_many(
                 char **passwords,
-                const void *salt, size_t salt_size,
-                const void *encrypted, size_t encrypted_size,
+                const char *xattr_value, size_t xattr_size,
                 const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
-                void **ret_decrypted, size_t *ret_decrypted_size) {
+                struct iovec *ret_decrypted) {
 
         int r;
 
         STRV_FOREACH(i, passwords) {
-                r = fscrypt_slot_try_one(*i, salt, salt_size, encrypted, encrypted_size, match_key_descriptor, ret_decrypted, ret_decrypted_size);
+                r = fscrypt_slot_try_one(*i, xattr_value, xattr_size, match_key_descriptor, ret_decrypted);
                 if (r != -ENOANO)
                         return r;
         }
@@ -303,8 +529,7 @@ static int fscrypt_setup(
                 const PasswordCache *cache,
                 char **password,
                 HomeSetup *setup,
-                void **ret_volume_key,
-                size_t *ret_volume_key_size) {
+                struct iovec *ret_volume_key) {
 
         _cleanup_free_ char *xattr_buf = NULL;
         int r;
@@ -317,10 +542,9 @@ static int fscrypt_setup(
                 return log_error_errno(r, "Failed to retrieve xattr list: %m");
 
         NULSTR_FOREACH(xa, xattr_buf) {
-                _cleanup_free_ void *salt = NULL, *encrypted = NULL;
                 _cleanup_free_ char *value = NULL;
-                size_t salt_size, encrypted_size, vsize;
-                const char *nr, *e;
+                size_t vsize;
+                const char *nr;
 
                 /* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32-bit unsigned integer */
                 nr = startswith(xa, "trusted.fscrypt_slot");
@@ -334,28 +558,17 @@ static int fscrypt_setup(
                         continue;
                 if (r < 0)
                         return log_error_errno(r, "Failed to read %s xattr: %m", xa);
-
-                e = memchr(value, ':', vsize);
-                if (!e)
-                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator.", xa);
-
-                r = unbase64mem_full(value, e - value, /* secure= */ false, &salt, &salt_size);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to decode salt of %s: %m", xa);
-
-                r = unbase64mem_full(e + 1, vsize - (e - value) - 1, /* secure= */ false, &encrypted, &encrypted_size);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa);
+                if (vsize == 0)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s is empty.", xa);
 
                 r = -ENOANO;
                 char **list;
                 FOREACH_ARGUMENT(list, cache->pkcs11_passwords, cache->fido2_passwords, password) {
                         r = fscrypt_slot_try_many(
                                         list,
-                                        salt, salt_size,
-                                        encrypted, encrypted_size,
+                                        value, vsize,
                                         setup->fscrypt_key_descriptor,
-                                        ret_volume_key, ret_volume_key_size);
+                                        ret_volume_key);
                         if (r >= 0)
                                 return 0;
                         if (r != -ENOANO)
@@ -371,9 +584,8 @@ int home_setup_fscrypt(
                 HomeSetup *setup,
                 const PasswordCache *cache) {
 
-        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        _cleanup_(iovec_done_erase) struct iovec volume_key = {};
         struct fscrypt_policy policy = {};
-        size_t volume_key_size = 0;
         const char *ip;
         int r;
 
@@ -404,8 +616,7 @@ int home_setup_fscrypt(
                         cache,
                         h->password,
                         setup,
-                        &volume_key,
-                        &volume_key_size);
+                        &volume_key);
         if (r < 0)
                 return r;
 
@@ -429,8 +640,8 @@ int home_setup_fscrypt(
 
                         r = fscrypt_upload_volume_key(
                                         setup->fscrypt_key_descriptor,
-                                        volume_key,
-                                        volume_key_size,
+                                        volume_key.iov_base,
+                                        volume_key.iov_len,
                                         KEY_SPEC_USER_KEYRING);
                         if (r < 0)
                                 _exit(EXIT_FAILURE);
@@ -476,11 +687,14 @@ static int fscrypt_slot_set(
                 const char *password,
                 uint32_t nr) {
 
-        _cleanup_free_ char *salt_base64 = NULL, *encrypted_base64 = NULL, *joined = NULL;
+        _cleanup_free_ char *salt_base64 = NULL, *iv_base64 = NULL,
+                            *encrypted_base64 = NULL, *tag_base64 = NULL,
+                            *joined = NULL;
         char label[STRLEN("trusted.fscrypt_slot") + DECIMAL_STR_MAX(nr) + 1];
         _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
-        int r, encrypted_size_out1, encrypted_size_out2;
-        uint8_t salt[64], derived[512 / 8] = {};
+        int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0;
+        uint8_t salt[FSCRYPT_SLOT_SALT_SIZE], iv[FSCRYPT_SLOT_GCM_IV_SIZE],
+                tag[FSCRYPT_SLOT_GCM_TAG_SIZE], derived[512 / 8] = {};
         _cleanup_free_ void *encrypted = NULL;
         const EVP_CIPHER *cc;
         size_t encrypted_size;
@@ -494,12 +708,16 @@ static int fscrypt_slot_set(
         if (r < 0)
                 return log_error_errno(r, "Failed to generate salt: %m");
 
+        r = crypto_random_bytes(iv, sizeof(iv));
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate IV: %m");
+
         CLEANUP_ERASE(derived);
 
         if (sym_PKCS5_PBKDF2_HMAC(
                             password, strlen(password),
                             salt, sizeof(salt),
-                            0xFFFF, sym_EVP_sha512(),
+                            (int) FSCRYPT_SLOT_PBKDF2_ITERATIONS, sym_EVP_sha512(),
                             sizeof(derived), derived) != 1)
                 return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
 
@@ -507,16 +725,24 @@ static int fscrypt_slot_set(
         if (!context)
                 return log_oom();
 
-        /* We use AES256 in counter mode */
-        cc = sym_EVP_aes_256_ctr();
+        /* AES-256-GCM: authenticated encryption with explicit random IV */
+        assert_se(cc = sym_EVP_aes_256_gcm());
 
-        /* We only use the first half of the derived key */
+        /* We only use the first 256 bit of the derived key */
         assert(sizeof(derived) >= (size_t) sym_EVP_CIPHER_get_key_length(cc));
 
-        if (sym_EVP_EncryptInit_ex(context, cc, NULL, derived, NULL) != 1)
+        if (sym_EVP_EncryptInit_ex(context, cc, /* impl= */ NULL, /* key= */ NULL, /* iv= */ NULL) != 1)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context.");
 
-        encrypted_size = volume_key_size + sym_EVP_CIPHER_get_key_length(cc) * 2;
+        if (sym_EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_SET_IVLEN, (int) sizeof(iv), /* ptr= */ NULL) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set GCM IV length.");
+
+        if (sym_EVP_EncryptInit_ex(context, /* type= */ NULL, /* impl= */ NULL, derived, iv) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set encryption key/IV.");
+
+        if (!ADD_SAFE(&encrypted_size, volume_key_size, (size_t) sym_EVP_CIPHER_get_block_size(cc)))
+                return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Encrypted buffer size would overflow.");
+
         encrypted = malloc(encrypted_size);
         if (!encrypted)
                 return log_oom();
@@ -529,19 +755,31 @@ static int fscrypt_slot_set(
         if (sym_EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of volume key.");
 
-        assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 < encrypted_size);
+        assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size);
         encrypted_size = (size_t) encrypted_size_out1 + (size_t) encrypted_size_out2;
 
+        if (sym_EVP_CIPHER_CTX_ctrl(context, EVP_CTRL_GCM_GET_TAG, (int) sizeof(tag), tag) != 1)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to retrieve GCM tag.");
+
         ss = base64mem(salt, sizeof(salt), &salt_base64);
         if (ss < 0)
                 return log_oom();
 
+        ss = base64mem(iv, sizeof(iv), &iv_base64);
+        if (ss < 0)
+                return log_oom();
+
         ss = base64mem(encrypted, encrypted_size, &encrypted_base64);
         if (ss < 0)
                 return log_oom();
 
-        joined = strjoin(salt_base64, ":", encrypted_base64);
-        if (!joined)
+        ss = base64mem(tag, sizeof(tag), &tag_base64);
+        if (ss < 0)
+                return log_oom();
+
+        if (asprintf(&joined, "$v2:%" PRIu32 ":%s:%s:%s:%s",
+                     FSCRYPT_SLOT_PBKDF2_ITERATIONS,
+                     salt_base64, iv_base64, encrypted_base64, tag_base64) < 0)
                 return log_oom();
 
         xsprintf(label, "trusted.fscrypt_slot%" PRIu32, nr);
@@ -716,9 +954,8 @@ int home_passwd_fscrypt(
                 const PasswordCache *cache,         /* the passwords acquired via PKCS#11/FIDO2 security tokens */
                 char **effective_passwords          /* new passwords */) {
 
-        _cleanup_(erase_and_freep) void *volume_key = NULL;
+        _cleanup_(iovec_done_erase) struct iovec volume_key = {};
         _cleanup_free_ char *xattr_buf = NULL;
-        size_t volume_key_size = 0;
         uint32_t slot = 0;
         int r;
 
@@ -730,13 +967,12 @@ int home_passwd_fscrypt(
                         cache,
                         h->password,
                         setup,
-                        &volume_key,
-                        &volume_key_size);
+                        &volume_key);
         if (r < 0)
                 return r;
 
         STRV_FOREACH(p, effective_passwords) {
-                r = fscrypt_slot_set(setup->root_fd, volume_key, volume_key_size, *p, slot);
+                r = fscrypt_slot_set(setup->root_fd, volume_key.iov_base, volume_key.iov_len, *p, slot);
                 if (r < 0)
                         return r;
 
index 0a6f02cc81b0a381bfe91d2276258f60a4b0b70a..0642eca2805a0f3fa952b77e565fcff10a86969d 100755 (executable)
@@ -1043,6 +1043,14 @@ testcase_fscrypt() {
     # After deactivation the fscrypt-encrypted directory is locked, so cleartext file names should not be visible
     [[ ! -e "$MNT/fscrypttest/file1" ]]
 
+    # Verify we actually use the v2 format
+    local SLOT
+    SLOT="$(getfattr --absolute-names --only-values -n trusted.fscrypt_slot0 "$MNT/fscrypttest")"
+    [[ "${SLOT:0:4}" == "\$v2:" ]] || {
+        echo "fscrypt slot 0 is not in v2 format: ${SLOT:0:32}"
+        return 1
+    }
+
     PASSWORD=fsfsfs1234 homectl activate fscrypttest
     [[ "$(fscrypt_run0 fsfsfs1234 'head -n1 /home/fscrypttest/file1')" == "hello fscrypt" ]]
     systemctl stop user@"$(id -u fscrypttest)".service 2>/dev/null || true