From cc7d9d04d6ce48e1088cf9c5096b6632a964526a Mon Sep 17 00:00:00 2001 From: Sebastien Buisson Date: Fri, 2 Aug 2024 15:03:49 +0200 Subject: [PATCH] LU-18087 enc: support encrypted names in changelogs In order to support encrypted names in changelogs, we choose to have the servers directly store the encoded+digested names in the changelogs. This requires to add some knowledge of encrypted names on server side, but that choice brings a number of benefits: - as servers are storing changelog records, they have access to the files' flags and can find out if files are encrypted or not. This would not be possible on client side when reading changelogs, because corresponding files might even no longer exist. - no modifications are needed on client side, either in the kernel or in userspace. As the API remains untouched, this is completely transparent to applications that are consuming changelogs. The file names retrieved from the changelogs are identical to the names displayed when listing directories without the encryption key. If names are not encrypted, they remain unchanged. If names are encrypted, their digested+encoded form is presented. Add sanity-sec test_73 to exercise this code. Signed-off-by: Sebastien Buisson Change-Id: If0de4bfa210f9067a7a934ac74863a77b19482db Reviewed-on: https://review.whamcloud.com/c/fs/lustre-release/+/55916 Reviewed-by: Mikhail Pershin Reviewed-by: Andreas Dilger Reviewed-by: Oleg Drokin Tested-by: jenkins Tested-by: Maloo --- libcfs/include/libcfs/crypto/llcrypt.h | 11 +- lustre/mdd/mdd_dir.c | 188 ++++++++++++++++++++++++++++++--- lustre/tests/sanity-sec.sh | 141 +++++++++++++++++++++++++ lustre/tests/test-framework.sh | 5 + 4 files changed, 326 insertions(+), 19 deletions(-) diff --git a/libcfs/include/libcfs/crypto/llcrypt.h b/libcfs/include/libcfs/crypto/llcrypt.h index c8c883c..997bee7 100644 --- a/libcfs/include/libcfs/crypto/llcrypt.h +++ b/libcfs/include/libcfs/crypto/llcrypt.h @@ -32,6 +32,11 @@ #define LL_CRYPTO_BLOCK_SIZE 16 +/* Extracts the second-to-last ciphertext block; see explanation below */ +#define LLCRYPT_FNAME_DIGEST(name, len) \ + ((name) + round_down((len) - LL_CRYPTO_BLOCK_SIZE - 1, \ + LL_CRYPTO_BLOCK_SIZE)) + struct llcrypt_ctx; struct llcrypt_info; @@ -197,12 +202,6 @@ extern int llcrypt_fname_disk_to_usr(struct inode *, u32, u32, const struct llcrypt_str *, struct llcrypt_str *); #define LLCRYPT_FNAME_MAX_UNDIGESTED_SIZE 32 - -/* Extracts the second-to-last ciphertext block; see explanation below */ -#define LLCRYPT_FNAME_DIGEST(name, len) \ - ((name) + round_down((len) - LL_CRYPTO_BLOCK_SIZE - 1, \ - LL_CRYPTO_BLOCK_SIZE)) - #define LLCRYPT_FNAME_DIGEST_SIZE LL_CRYPTO_BLOCK_SIZE /** diff --git a/lustre/mdd/mdd_dir.c b/lustre/mdd/mdd_dir.c index 72c314a..9328652 100644 --- a/lustre/mdd/mdd_dir.c +++ b/lustre/mdd/mdd_dir.c @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include "mdd_internal.h" @@ -1140,6 +1142,120 @@ static int mdd_changelog_ns_pfid_set(const struct lu_env *env, return rc; } +/* The digested form is made of a FID (16 bytes) followed by the second-to-last + * ciphertext block (16 bytes), so a total length of 32 bytes. + */ +/* Must be identical to ll_digest_filename in llite_internal.h */ +struct changelog_digest_filename { + struct lu_fid cdf_fid; + char cdf_excerpt[LL_CRYPTO_BLOCK_SIZE]; +}; + +/** + * Utility function to process filename in changelog + * + * \param[in] name file name + * \param[in] namelen file name len + * \param[in] fid file FID + * \param[in] enc is object encrypted? + * \param[out]ln pointer to the struct lu_name to hold the real name + * + * If file is not encrypted, output name is just the file name. + * If file is encrypted, file name needs to be decoded then digested if the name + * is also encrypted. In this case a new buffer is allocated, and ln->ln_name + * needs to be freed by the caller. + * + * \retval 0, on success + * \retval -ve, on error + */ +static int changelog_name2digest(const char *name, int namelen, + const struct lu_fid *fid, + bool enc, struct lu_name *ln) +{ + struct changelog_digest_filename *digest = NULL; + char *buf = NULL, *bufout = NULL, *p, *q; + int len, bufoutlen; + int rc = 0; + + ENTRY; + + ln->ln_name = name; + ln->ln_namelen = namelen; + + if (!enc) + GOTO(out, rc); + + /* now we know file is encrypted */ + if (strnchr(name, namelen, '=')) { + /* only proceed to critical decode if + * encrypted name contains espace char '=' + */ + buf = kmalloc(namelen, GFP_NOFS); + if (!buf) + GOTO(out, rc = -ENOMEM); + + namelen = critical_decode(name, namelen, buf); + ln->ln_name = buf; + ln->ln_namelen = namelen; + } + + p = (char *)ln->ln_name; + len = namelen; + while (len--) { + if (!isprint(*p++)) + break; + } + + /* len == -1 means we went through the whole decoded name without + * finding any non-printable character, so consider it is not encrypted + */ + if (len == -1) + GOTO(out, rc); + + /* now we know the name has some non-printable characters */ + if (namelen > LL_CRYPTO_BLOCK_SIZE * 2) { + if (!fid) + GOTO(out, rc = -EPROTO); + + OBD_ALLOC_PTR(digest); + if (!digest) + GOTO(out, rc = -ENOMEM); + + digest->cdf_fid = *fid; + memcpy(digest->cdf_excerpt, + LLCRYPT_FNAME_DIGEST(ln->ln_name, ln->ln_namelen), + LL_CRYPTO_BLOCK_SIZE); + p = (char *)digest; + len = sizeof(*digest); + } else { + p = (char *)ln->ln_name; + len = ln->ln_namelen; + } + + bufoutlen = BASE64URL_CHARS(len) + 2; + bufout = kmalloc(digest ? bufoutlen + 1 : bufoutlen, GFP_NOFS); + if (!bufout) + GOTO(free_digest, rc = -ENOMEM); + + q = bufout; + if (digest) + *q++ = LLCRYPT_DIGESTED_CHAR; + /* beware that gss_base64url_encode adds a trailing space */ + gss_base64url_encode(&q, &bufoutlen, (__u8 *)p, len); + if (bufoutlen == -1) { + kfree(bufout); + } else { + kfree(buf); + ln->ln_name = bufout; + ln->ln_namelen = q - bufout - 1; + } + +free_digest: + OBD_FREE_PTR(digest); +out: + RETURN(rc); +} + /** Store a namespace change changelog record * If this fails, we must fail the whole transaction; we don't * want the change to commit without the log entry. @@ -1167,12 +1283,16 @@ int mdd_changelog_ns_store(const struct lu_env *env, const struct lu_name *sname, struct thandle *handle) { + struct mdd_thread_info *info = mdd_env_info(env); + struct lu_name *ltname = NULL, *lsname = NULL; const struct lu_ucred *uc = lu_ucred(env); struct llog_changelog_rec *rec; + __u64 xflags = CLFE_INVALID; + struct lu_fid *tfid = NULL; struct lu_buf *buf; int reclen; - __u64 xflags = CLFE_INVALID; - int rc; + bool enc; + int rc = 0; ENTRY; @@ -1183,10 +1303,42 @@ int mdd_changelog_ns_store(const struct lu_env *env, LASSERT(tname != NULL); LASSERT(handle != NULL); - reclen = mdd_llog_record_calc_size(env, tname, sname); + if (tname) { + OBD_ALLOC_PTR(ltname); + if (!ltname) + GOTO(out, rc = -ENOMEM); + + if (sname) { + enc = info->mdi_tpattr.la_valid & LA_FLAGS && + info->mdi_tpattr.la_flags & LUSTRE_ENCRYPT_FL; + tfid = (struct lu_fid *)sfid; + } else { + enc = info->mdi_pattr.la_valid & LA_FLAGS && + info->mdi_pattr.la_flags & LUSTRE_ENCRYPT_FL; + tfid = (struct lu_fid *)mdd_object_fid(target); + } + rc = changelog_name2digest(tname->ln_name, tname->ln_namelen, + tfid, enc, ltname); + if (rc) + GOTO(out_ltname, rc); + } + if (sname) { + OBD_ALLOC_PTR(lsname); + if (!lsname) + GOTO(out_ltname, rc = -ENOMEM); + + enc = info->mdi_pattr.la_valid & LA_FLAGS && + info->mdi_pattr.la_flags & LUSTRE_ENCRYPT_FL; + rc = changelog_name2digest(sname->ln_name, sname->ln_namelen, + tfid, enc, lsname); + if (rc) + GOTO(out_lsname, rc); + } + + reclen = mdd_llog_record_calc_size(env, ltname, lsname); buf = lu_buf_check_and_alloc(&mdd_env_info(env)->mdi_chlg_buf, reclen); if (buf->lb_buf == NULL) - RETURN(-ENOMEM); + GOTO(out_lsname, rc = -ENOMEM); rec = buf->lb_buf; clf_flags &= CLF_FLAGMASK; @@ -1200,7 +1352,7 @@ int mdd_changelog_ns_store(const struct lu_env *env, xflags |= CLFE_NID_BE; } - if (sname != NULL) + if (lsname != NULL) clf_flags |= CLF_RENAME; else clf_flags |= CLF_VERSION; @@ -1221,10 +1373,11 @@ int mdd_changelog_ns_store(const struct lu_env *env, rc = mdd_changelog_ns_pfid_set(env, mdd, parent, pattr, &rec->cr.cr_pfid); if (rc < 0) - RETURN(rc); + GOTO(out_lsname, rc); - rec->cr.cr_namelen = tname->ln_namelen; - memcpy(changelog_rec_name(&rec->cr), tname->ln_name, tname->ln_namelen); + rec->cr.cr_namelen = ltname->ln_namelen; + memcpy(changelog_rec_name(&rec->cr), ltname->ln_name, + ltname->ln_namelen); if (clf_flags & CLF_RENAME) { struct lu_fid spfid; @@ -1232,9 +1385,9 @@ int mdd_changelog_ns_store(const struct lu_env *env, rc = mdd_changelog_ns_pfid_set(env, mdd, sparent, spattr, &spfid); if (rc < 0) - RETURN(rc); + GOTO(out_lsname, rc); - mdd_changelog_rec_ext_rename(&rec->cr, sfid, &spfid, sname); + mdd_changelog_rec_ext_rename(&rec->cr, sfid, &spfid, lsname); } if (clf_flags & CLF_JOBID) @@ -1251,12 +1404,21 @@ int mdd_changelog_ns_store(const struct lu_env *env, if (rc < 0) { CERROR("%s: cannot store changelog record: type = %d, name = '%s', t = " DFID", p = "DFID": rc = %d\n", - mdd2obd_dev(mdd)->obd_name, type, tname->ln_name, + mdd2obd_dev(mdd)->obd_name, type, ltname->ln_name, PFID(&rec->cr.cr_tfid), PFID(&rec->cr.cr_pfid), rc); - return -EFAULT; + GOTO(out_lsname, rc = -EFAULT); } - return 0; +out_lsname: + if (lsname && lsname->ln_name != sname->ln_name) + kfree(lsname->ln_name); + OBD_FREE_PTR(lsname); +out_ltname: + if (ltname && ltname->ln_name != tname->ln_name) + kfree(ltname->ln_name); + OBD_FREE_PTR(ltname); +out: + RETURN(rc); } static int __mdd_links_add(const struct lu_env *env, diff --git a/lustre/tests/sanity-sec.sh b/lustre/tests/sanity-sec.sh index b5c3092..eb65976 100755 --- a/lustre/tests/sanity-sec.sh +++ b/lustre/tests/sanity-sec.sh @@ -6749,6 +6749,147 @@ test_72() { } run_test 72 "dynamic nodemap properties" +test_73() { + local vaultdir1=$DIR/$tdir/vault1 + local vaultdir2=$DIR/$tdir/vault2 + local shortfname="short=a" + local longfname="longfilenamewitha=inthemiddletotestbehaviorregardingthedigestedform" + local fid + local digshort1 + local digshort2 + local diglong1 + local diglong2 + + (( $MDS1_VERSION >= $(version_code 2.16.50) )) || + skip "Need MDS version at least 2.16.50" + + [[ $($LCTL get_param mdc.*.import) =~ client_encryption ]] || + skip "need encryption support" + which fscrypt || skip_env "Need fscrypt" + + mkdir -p $DIR/$tdir || error "mkdir $DIR/$tdir failed" + + yes | fscrypt setup --force --verbose || + echo "fscrypt global setup already done" + sed -i 's/\(.*\)policy_version\(.*\):\(.*\)\"[0-9]*\"\(.*\)/\1policy_version\2:\3"2"\4/' \ + /etc/fscrypt.conf + yes | fscrypt setup --verbose $MOUNT || + echo "fscrypt setup $MOUNT already done" + stack_trap "rm -rf $MOUNT/.fscrypt" + + # enable_filename_encryption tunable only available for client + # built against embedded llcrypt. If client is built against in-kernel + # fscrypt, file names are always encrypted. + $LCTL get_param mdc.*.connect_flags | grep -q name_encryption && + nameenc=$(lctl get_param -n llite.*.enable_filename_encryption | + head -n1) + + # begin with non-encrypted names + if [ -n "$nameenc" ] && (( nameenc != 0 )); then + $LCTL set_param llite.*.enable_filename_encryption=0 + [ $? -eq 0 ] || + error "set_param \ + llite.*.enable_filename_encryption=1 failed" + fi + + mkdir -p $vaultdir1 + stack_trap "rm -rf $vaultdir1" + + echo -e 'mypass\nmypass' | fscrypt encrypt --verbose \ + --source=custom_passphrase --name=protector_73a $vaultdir1 || + error "fscrypt encrypt $vaultdir1 failed" + + # activate changelogs + changelog_register || error "changelog_register failed" + local cl_user="${CL_USERS[$SINGLEMDS]%% *}" + changelog_users $SINGLEMDS | grep -q $cl_user || + error "User $cl_user not found in changelog_users" + changelog_chmask ALL + + touch $vaultdir1/$shortfname || + error "touch $vaultdir1/$shortfname failed" + fid=$($LFS path2fid $vaultdir1/$shortfname) + fid="${fid:1:-1}" + fscrypt lock $vaultdir1 || error "fscrypt lock $vaultdir1 failed" + digshort1=$($LFS fid2path $MOUNT $fid) + digshort1=$(basename $digshort1) + echo mypass | fscrypt unlock $vaultdir1 || + error "fscrypt unlock $vaultdir1 failed" + mrename $vaultdir1/$shortfname $vaultdir1/$longfname || + error "mrename $vaultdir1/$shortfname failed" + fscrypt lock $vaultdir1 || error "fscrypt lock $vaultdir1 failed" + diglong1=$($LFS fid2path $MOUNT $fid) + diglong1=$(basename $diglong1) + + # access changelogs + echo "changelogs dump" + changelog_dump || error "failed to dump changelogs" + digshort2=$(changelog_find -type CREAT -target-fid $fid | + awk '{print $12}') + [[ $digshort1 == $digshort2 ]] || + error "name $digshort2 in CREAT is not $digshort1" + digshort2=$(changelog_find -type RENME -source-fid $fid | + awk '{print $15}') + [[ $digshort1 == $digshort2 ]] || + error "name $digshort2 in RENME is not $digshort1" + diglong2=$(changelog_find -type RENME -source-fid $fid | + awk '{print $12}') + [[ $diglong1 == $diglong2 ]] || + error "name $diglong2 in RENME is not $diglong1" + + echo "changelogs clear" + changelog_clear 0 || error "failed to clear changelogs" + + # now switch to encrypted names + if [ -n "$nameenc" ] && (( nameenc != 1 )); then + $LCTL set_param llite.*.enable_filename_encryption=1 + [ $? -eq 0 ] || + error "set_param \ + llite.*.enable_filename_encryption=1 failed" + stack_trap \ + "$LCTL set_param llite.*.enable_filename_encryption=0" + fi + + $LFS mkdir -c1 -i $((MDSCOUNT-1)) $vaultdir2 + stack_trap "rm -rf $vaultdir2" + + echo -e 'mypass\nmypass' | fscrypt encrypt --verbose \ + --source=custom_passphrase --name=protector_73b $vaultdir2 || + error "fscrypt encrypt $vaultdir2 failed" + + touch $vaultdir2/$shortfname || + error "touch $vaultdir2/$shortfname failed" + fid=$($LFS path2fid $vaultdir2/$shortfname) + fid="${fid:1:-1}" + fscrypt lock $vaultdir2 || error "fscrypt lock $vaultdir2 failed" + digshort1=$($LFS fid2path $MOUNT $fid) + digshort1=$(basename $digshort1) + echo mypass | fscrypt unlock $vaultdir2 || + error "fscrypt unlock $vaultdir2 failed" + mrename $vaultdir2/$shortfname $vaultdir2/$longfname || + error "mrename $vaultdir2/$shortfname failed" + fscrypt lock $vaultdir2 || error "fscrypt lock $vaultdir2 failed" + diglong1=$($LFS fid2path $MOUNT $fid) + diglong1=$(basename $diglong1) + + # access changelogs + echo "changelogs dump" + changelog_dump || error "failed to dump changelogs" + digshort2=$(changelog_find -type CREAT -target-fid $fid | + awk '{print $12}') + [[ $digshort1 == $digshort2 ]] || + error "name $digshort2 in CREAT is not $digshort1" + digshort2=$(changelog_find -type RENME -source-fid $fid | + awk '{print $15}') + [[ $digshort1 == $digshort2 ]] || + error "name $digshort2 in RENME is not $digshort1" + diglong2=$(changelog_find -type RENME -source-fid $fid | + awk '{print $12}') + [[ $diglong1 == $diglong2 ]] || + error "name $diglong2 in RENME is not $diglong1" +} +run_test 73 "encrypted names in changelogs" + log "cleanup: ======================================================" sec_unsetup() { diff --git a/lustre/tests/test-framework.sh b/lustre/tests/test-framework.sh index ccd49be..952571b 100755 --- a/lustre/tests/test-framework.sh +++ b/lustre/tests/test-framework.sh @@ -11245,6 +11245,11 @@ changelog2array() x) key=xattr ;; + s) + key=source-fid + value="${value#[}" + value="${value%]}" + ;; *) ;; esac -- 1.8.3.1