Whamcloud - gitweb
LU-18087 enc: support encrypted names in changelogs 16/55916/8
authorSebastien Buisson <sbuisson@ddn.com>
Fri, 2 Aug 2024 13:03:49 +0000 (15:03 +0200)
committerOleg Drokin <green@whamcloud.com>
Thu, 2 Jan 2025 20:47:08 +0000 (20:47 +0000)
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 <sbuisson@ddn.com>
Change-Id: If0de4bfa210f9067a7a934ac74863a77b19482db
Reviewed-on: https://review.whamcloud.com/c/fs/lustre-release/+/55916
Reviewed-by: Mikhail Pershin <mpershin@whamcloud.com>
Reviewed-by: Andreas Dilger <adilger@whamcloud.com>
Reviewed-by: Oleg Drokin <green@whamcloud.com>
Tested-by: jenkins <devops@whamcloud.com>
Tested-by: Maloo <maloo@whamcloud.com>
libcfs/include/libcfs/crypto/llcrypt.h
lustre/mdd/mdd_dir.c
lustre/tests/sanity-sec.sh
lustre/tests/test-framework.sh

index c8c883c..997bee7 100644 (file)
 
 #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
 
 /**
index 72c314a..9328652 100644 (file)
@@ -23,6 +23,8 @@
 #include <lustre_fid.h>
 #include <lustre_lmv.h>
 #include <lustre_idmap.h>
+#include <lustre_crypto.h>
+#include <uapi/linux/lustre/lgss.h>
 
 #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,
index b5c3092..eb65976 100755 (executable)
@@ -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() {
index ccd49be..952571b 100755 (executable)
@@ -11245,6 +11245,11 @@ changelog2array()
                x)
                        key=xattr
                        ;;
+               s)
+                       key=source-fid
+                       value="${value#[}"
+                       value="${value%]}"
+                       ;;
                *)
                        ;;
                esac