ceph: create symlinks with encrypted and base64-encoded targets
authorJeff Layton <jlayton@kernel.org>
Thu, 3 Sep 2020 17:31:10 +0000 (13:31 -0400)
committerIlya Dryomov <idryomov@gmail.com>
Thu, 24 Aug 2023 09:24:35 +0000 (11:24 +0200)
When creating symlinks in encrypted directories, encrypt and
base64-encode the target with the new inode's key before sending to the
MDS.

When filling a symlinked inode, base64-decode it into a buffer that
we'll keep in ci->i_symlink. When get_link is called, decrypt the buffer
into a new one that will hang off i_link.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
Reviewed-by: Xiubo Li <xiubli@redhat.com>
Reviewed-and-tested-by: Luís Henriques <lhenriques@suse.de>
Reviewed-by: Milind Changire <mchangir@redhat.com>
Signed-off-by: Ilya Dryomov <idryomov@gmail.com>
fs/ceph/dir.c
fs/ceph/inode.c

index ee5c8b9..7e064c0 100644 (file)
@@ -948,6 +948,43 @@ static int ceph_create(struct mnt_idmap *idmap, struct inode *dir,
        return ceph_mknod(idmap, dir, dentry, mode, 0);
 }
 
+#if IS_ENABLED(CONFIG_FS_ENCRYPTION)
+static int prep_encrypted_symlink_target(struct ceph_mds_request *req,
+                                        const char *dest)
+{
+       int err;
+       int len = strlen(dest);
+       struct fscrypt_str osd_link = FSTR_INIT(NULL, 0);
+
+       err = fscrypt_prepare_symlink(req->r_parent, dest, len, PATH_MAX,
+                                     &osd_link);
+       if (err)
+               goto out;
+
+       err = fscrypt_encrypt_symlink(req->r_new_inode, dest, len, &osd_link);
+       if (err)
+               goto out;
+
+       req->r_path2 = kmalloc(CEPH_BASE64_CHARS(osd_link.len) + 1, GFP_KERNEL);
+       if (!req->r_path2) {
+               err = -ENOMEM;
+               goto out;
+       }
+
+       len = ceph_base64_encode(osd_link.name, osd_link.len, req->r_path2);
+       req->r_path2[len] = '\0';
+out:
+       fscrypt_fname_free_buffer(&osd_link);
+       return err;
+}
+#else
+static int prep_encrypted_symlink_target(struct ceph_mds_request *req,
+                                        const char *dest)
+{
+       return -EOPNOTSUPP;
+}
+#endif
+
 static int ceph_symlink(struct mnt_idmap *idmap, struct inode *dir,
                        struct dentry *dentry, const char *dest)
 {
@@ -983,14 +1020,21 @@ static int ceph_symlink(struct mnt_idmap *idmap, struct inode *dir,
                goto out_req;
        }
 
-       req->r_path2 = kstrdup(dest, GFP_KERNEL);
-       if (!req->r_path2) {
-               err = -ENOMEM;
-               goto out_req;
-       }
        req->r_parent = dir;
        ihold(dir);
 
+       if (IS_ENCRYPTED(req->r_new_inode)) {
+               err = prep_encrypted_symlink_target(req, dest);
+               if (err)
+                       goto out_req;
+       } else {
+               req->r_path2 = kstrdup(dest, GFP_KERNEL);
+               if (!req->r_path2) {
+                       err = -ENOMEM;
+                       goto out_req;
+               }
+       }
+
        set_bit(CEPH_MDS_R_PARENT_LOCKED, &req->r_req_flags);
        req->r_dentry = dget(dentry);
        req->r_num_caps = 2;
index 0c33809..5423c69 100644 (file)
@@ -35,6 +35,7 @@
  */
 
 static const struct inode_operations ceph_symlink_iops;
+static const struct inode_operations ceph_encrypted_symlink_iops;
 
 static void ceph_inode_work(struct work_struct *work);
 
@@ -640,6 +641,7 @@ void ceph_free_inode(struct inode *inode)
 #ifdef CONFIG_FS_ENCRYPTION
        kfree(ci->fscrypt_auth);
 #endif
+       fscrypt_free_inode(inode);
        kmem_cache_free(ceph_inode_cachep, ci);
 }
 
@@ -837,6 +839,34 @@ void ceph_fill_file_time(struct inode *inode, int issued,
                     inode, time_warp_seq, ci->i_time_warp_seq);
 }
 
+#if IS_ENABLED(CONFIG_FS_ENCRYPTION)
+static int decode_encrypted_symlink(const char *encsym, int enclen, u8 **decsym)
+{
+       int declen;
+       u8 *sym;
+
+       sym = kmalloc(enclen + 1, GFP_NOFS);
+       if (!sym)
+               return -ENOMEM;
+
+       declen = ceph_base64_decode(encsym, enclen, sym);
+       if (declen < 0) {
+               pr_err("%s: can't decode symlink (%d). Content: %.*s\n",
+                      __func__, declen, enclen, encsym);
+               kfree(sym);
+               return -EIO;
+       }
+       sym[declen + 1] = '\0';
+       *decsym = sym;
+       return declen;
+}
+#else
+static int decode_encrypted_symlink(const char *encsym, int symlen, u8 **decsym)
+{
+       return -EOPNOTSUPP;
+}
+#endif
+
 /*
  * Populate an inode based on info from mds.  May be called on new or
  * existing inodes.
@@ -1071,26 +1101,42 @@ int ceph_fill_inode(struct inode *inode, struct page *locked_page,
                inode->i_fop = &ceph_file_fops;
                break;
        case S_IFLNK:
-               inode->i_op = &ceph_symlink_iops;
                if (!ci->i_symlink) {
                        u32 symlen = iinfo->symlink_len;
                        char *sym;
 
                        spin_unlock(&ci->i_ceph_lock);
 
-                       if (symlen != i_size_read(inode)) {
-                               pr_err("%s %llx.%llx BAD symlink "
-                                       "size %lld\n", __func__,
-                                       ceph_vinop(inode),
-                                       i_size_read(inode));
+                       if (IS_ENCRYPTED(inode)) {
+                               if (symlen != i_size_read(inode))
+                                       pr_err("%s %llx.%llx BAD symlink size %lld\n",
+                                               __func__, ceph_vinop(inode),
+                                               i_size_read(inode));
+
+                               err = decode_encrypted_symlink(iinfo->symlink,
+                                                              symlen, (u8 **)&sym);
+                               if (err < 0) {
+                                       pr_err("%s decoding encrypted symlink failed: %d\n",
+                                               __func__, err);
+                                       goto out;
+                               }
+                               symlen = err;
                                i_size_write(inode, symlen);
                                inode->i_blocks = calc_inode_blocks(symlen);
-                       }
+                       } else {
+                               if (symlen != i_size_read(inode)) {
+                                       pr_err("%s %llx.%llx BAD symlink size %lld\n",
+                                               __func__, ceph_vinop(inode),
+                                               i_size_read(inode));
+                                       i_size_write(inode, symlen);
+                                       inode->i_blocks = calc_inode_blocks(symlen);
+                               }
 
-                       err = -ENOMEM;
-                       sym = kstrndup(iinfo->symlink, symlen, GFP_NOFS);
-                       if (!sym)
-                               goto out;
+                               err = -ENOMEM;
+                               sym = kstrndup(iinfo->symlink, symlen, GFP_NOFS);
+                               if (!sym)
+                                       goto out;
+                       }
 
                        spin_lock(&ci->i_ceph_lock);
                        if (!ci->i_symlink)
@@ -1098,7 +1144,17 @@ int ceph_fill_inode(struct inode *inode, struct page *locked_page,
                        else
                                kfree(sym); /* lost a race */
                }
-               inode->i_link = ci->i_symlink;
+
+               if (IS_ENCRYPTED(inode)) {
+                       /*
+                        * Encrypted symlinks need to be decrypted before we can
+                        * cache their targets in i_link. Don't touch it here.
+                        */
+                       inode->i_op = &ceph_encrypted_symlink_iops;
+               } else {
+                       inode->i_link = ci->i_symlink;
+                       inode->i_op = &ceph_symlink_iops;
+               }
                break;
        case S_IFDIR:
                inode->i_op = &ceph_dir_iops;
@@ -2126,6 +2182,32 @@ static void ceph_inode_work(struct work_struct *work)
        iput(inode);
 }
 
+static const char *ceph_encrypted_get_link(struct dentry *dentry,
+                                          struct inode *inode,
+                                          struct delayed_call *done)
+{
+       struct ceph_inode_info *ci = ceph_inode(inode);
+
+       if (!dentry)
+               return ERR_PTR(-ECHILD);
+
+       return fscrypt_get_symlink(inode, ci->i_symlink, i_size_read(inode),
+                                  done);
+}
+
+static int ceph_encrypted_symlink_getattr(struct mnt_idmap *idmap,
+                                         const struct path *path,
+                                         struct kstat *stat, u32 request_mask,
+                                         unsigned int query_flags)
+{
+       int ret;
+
+       ret = ceph_getattr(idmap, path, stat, request_mask, query_flags);
+       if (ret)
+               return ret;
+       return fscrypt_symlink_getattr(path, stat);
+}
+
 /*
  * symlinks
  */
@@ -2136,6 +2218,13 @@ static const struct inode_operations ceph_symlink_iops = {
        .listxattr = ceph_listxattr,
 };
 
+static const struct inode_operations ceph_encrypted_symlink_iops = {
+       .get_link = ceph_encrypted_get_link,
+       .setattr = ceph_setattr,
+       .getattr = ceph_encrypted_symlink_getattr,
+       .listxattr = ceph_listxattr,
+};
+
 int __ceph_setattr(struct inode *inode, struct iattr *attr,
                   struct ceph_iattr *cia)
 {