#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"
#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"
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;
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;
}
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;
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");
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)
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;
cache,
h->password,
setup,
- &volume_key,
- &volume_key_size);
+ &volume_key);
if (r < 0)
return r;
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);
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;
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");
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();
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);
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;
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;