homed/fscrypt: add new xattr format hardening key sealing
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
This commit is contained in:
@@ -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.
|
||||
|
||||
+288
-52
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user