// SPDX-License-Identifier: GPL-2.0
/*
 * linux/fs/ext4/xattr_security.c
 * Handler for storing security labels as extended attributes.
 */

#include <linux/string.h>
#include <linux/fs.h>
#include <linux/security.h>
#include <linux/slab.h>
#include "ext4_jbd2.h"
#include "ext4.h"
#include "critical_encode.h"
#include "encdata.h"
#include "xattr.h"

/* security.encdata is a virtual xattr containing information related
 * to encrypted files. It is expressed as ASCII text with a "key: value"
 * format, and space as field separator. For instance:
 *
 *    { encoding: base64url, size: 3012, enc_ctx: YWJjZGVmZ2hpamtsbW
 *    5vcHFyc3R1dnd4eXphYmNkZWZnaGlqa2xtbg, enc_name: ZmlsZXdpdGh2ZX
 *    J5bG9uZ25hbWVmaWxld2l0aHZlcnlsb25nbmFtZWZpbGV3aXRodmVyeWxvbmdu
 *    YW1lZmlsZXdpdGg }
 *
 * 'encoding' is the encoding method used for binary data, assume name
 * can be up to 255 chars.
 * 'size' is the clear text file data length in bytes.
 * 'enc_ctx' is encoded encryption context, 40 bytes for v2.
 * 'enc_name' is encoded encrypted name, 256 bytes max.
 * So on overall, this xattr is at most 727 chars plus terminating '\0'.
 */
static int ext4_build_xattr_encdata(struct dentry *dentry,
				     struct inode *inode,
				     void *buffer, size_t size)
{
	char encoded_enc_ctx[BASE64URL_CHARS(LLCRYPT_SET_CONTEXT_MAX_SIZE) + 1];
	unsigned char enc_ctx[LLCRYPT_SET_CONTEXT_MAX_SIZE];
	char encoded_name[BASE64URL_CHARS(NAME_MAX) + 1];
	struct ext4_filename fname = { 0 };
	struct inode *parent = NULL;
	int encoded_enc_ctx_len = 0;
	int encoded_name_len = 0;
	char size_str[32];
	int retval;

	if (!IS_ENCRYPTED(inode)) {
		retval = -ENODATA;
		goto out;
	}

	/* get size */
	retval = snprintf(size_str, sizeof(size_str), "%llu",
			  S_ISDIR(inode->i_mode) ? 0 : inode->i_size);
	if (retval >= sizeof(size_str)) {
		retval = -ERANGE;
		goto out;
	}

	/* get raw name */
	if (dentry && dentry->d_parent)
		parent = dentry->d_parent->d_inode;

	retval = ext4_setup_filename(parent, &dentry->d_name, 1, &fname);
	if (retval)
		goto out;

	/* base64url-encode raw name */
	encoded_name_len = encode(fname.disk_name.name, fname.disk_name.len,
				  encoded_name, ENCDATA_ENCODING);
	if (encoded_name_len == -EINVAL) {
		retval = -EINVAL;
		goto out;
	}
	encoded_name[encoded_name_len] = '\0';

	if (!buffer) {
		/* Return exact xattr length we would return if called with
		 * non-NULL buffer.
		 */
		retval = sizeof(ENCDATA_XATTR_FMT_COMP) - 1 +
			sizeof(ENCDATA_ENCODING) - 1 + strlen(size_str) +
			BASE64URL_CHARS(LLCRYPT_SET_CONTEXT_MAX_SIZE) +
			encoded_name_len;
		goto out;
	}

	/* get encryption context */
	retval = ext4_xattr_get(inode, EXT4_XATTR_INDEX_ENCRYPTION,
				EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
				enc_ctx, sizeof(enc_ctx));

	if (retval < 0)
		goto out;

	/* base64url-encode encryption context */
	encoded_enc_ctx_len = encode(enc_ctx, retval, encoded_enc_ctx,
				     ENCDATA_ENCODING);
	if (encoded_enc_ctx_len == -EINVAL) {
		retval = -EINVAL;
		goto out;
	}
	encoded_enc_ctx[encoded_enc_ctx_len] = '\0';

	/* write EXT4_XATTR_ENCDATA info into buffer */
	retval = snprintf(buffer, size,
			  ENCDATA_XATTR_FMT_1 ENCDATA_ENCODING
			  ENCDATA_XATTR_FMT_2"%s"ENCDATA_XATTR_FMT_3"%s"
			  ENCDATA_XATTR_FMT_4"%s"ENCDATA_XATTR_FMT_END,
			  size_str, encoded_enc_ctx,
			  encoded_name_len ? encoded_name : "");
	if (retval >= size)
		retval = -ERANGE;

out:
	if (fname.disk_name.name != dentry->d_name.name)
		kfree(fname.disk_name.name);

	return retval;
}

static int ext4_process_xattr_encdata(struct inode *inode,
				       const void *value, size_t size,
				       int flags)
{
	char encoded_enc_ctx[BASE64URL_CHARS(LLCRYPT_SET_CONTEXT_MAX_SIZE) + 1];
	unsigned char enc_ctx[LLCRYPT_SET_CONTEXT_MAX_SIZE];
	char encoded_name[BASE64URL_CHARS(NAME_MAX) + 1];
	char encoding[NAME_MAX + 1];
	char name[NAME_MAX + 1];
	loff_t disk_size = 0;
	char *buffer = NULL;
	int enc_ctx_len = 0;
	int name_len = 0;
	int retval = 0;

	if (IS_ENCRYPTED(inode) || !value || flags & XATTR_REPLACE) {
		retval = -EINVAL;
		goto out;
	}

	buffer = kmalloc(size + 1, GFP_NOFS);
	if (!buffer) {
		retval = -ENOMEM;
		goto out;
	}
	memcpy(buffer, value, size);
	buffer[size] = '\0';

	retval = sscanf(buffer, encdata_xattr_fmt,
			encoding, &disk_size, encoded_enc_ctx, encoded_name);
	if (retval < 4) {
		retval = -EINVAL;
		goto out;
	}

	/* get former encryption context: should not exist */
	retval = ext4_xattr_get(inode, EXT4_XATTR_INDEX_ENCRYPTION,
				EXT4_XATTR_NAME_ENCRYPTION_CONTEXT, NULL, 0);
	if (retval != -ENODATA) {
		retval = -EINVAL;
		goto out;
	}

	if (strlen(encoded_enc_ctx) >
	    BASE64URL_CHARS(LLCRYPT_SET_CONTEXT_MAX_SIZE)) {
		retval = -EINVAL;
		goto out;
	}

	/* base64url-decode encryption context */
	retval = decode(encoded_enc_ctx, strlen(encoded_enc_ctx),
			enc_ctx, encoding);
	if (retval < 0) {
		retval = -EINVAL;
		goto out;
	}
	enc_ctx_len = retval;

	/* set encryption context, this will set encryption flag */
	retval = ext4_xattr_set(inode, EXT4_XATTR_INDEX_ENCRYPTION,
				EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
				enc_ctx, enc_ctx_len, XATTR_CREATE);
	if (retval < 0)
		goto out;

	if (disk_size) {
		/* set size on inode */
		spin_lock(&inode->i_lock);
		i_size_write(inode, disk_size);
		EXT4_I(inode)->i_disksize = disk_size;
		spin_unlock(&inode->i_lock);
		mark_inode_dirty(inode);
	}

	/* put raw encrypted name in EXT4_XATTR_NAME_RAWENCNAME xattr,
	 * for later use, but base64url-decode first
	 */
	retval = decode(encoded_name, strlen(encoded_name), name, encoding);
	if (retval < 0) {
		retval = -EINVAL;
		goto out;
	}
	name_len = retval;

	retval = ext4_xattr_set(inode, EXT4_XATTR_INDEX_LUSTRE,
				EXT4_XATTR_NAME_RAWENCNAME,
				name, name_len, XATTR_CREATE);

out:
	kfree(buffer);

	return retval;
}

static int
ext4_xattr_security_get(const struct xattr_handler *handler,
			   struct dentry *dentry, struct inode *inode,
			const char *name, void *buffer, size_t size)
{
	if (!strncmp(name, EXT4_XATTR_NAME_ENCDATA, strlen(name)))
		return ext4_build_xattr_encdata(dentry, inode, buffer, size);

	return ext4_xattr_get(inode, EXT4_XATTR_INDEX_SECURITY,
			      name, buffer, size);
}

static int
ext4_xattr_security_set(const struct xattr_handler *handler,
			struct mnt_idmap *idmap,
			struct dentry *unused, struct inode *inode,
			const char *name, const void *value,
			size_t size, int flags)
{
	if (!strncmp(name, EXT4_XATTR_NAME_ENCDATA, strlen(name)))
		return ext4_process_xattr_encdata(inode, value, size, flags);

	return ext4_xattr_set(inode, EXT4_XATTR_INDEX_SECURITY,
			      name, value, size, flags);
}

static int
ext4_initxattrs(struct inode *inode, const struct xattr *xattr_array,
		void *fs_info)
{
	const struct xattr *xattr;
	handle_t *handle = fs_info;
	int err = 0;

	for (xattr = xattr_array; xattr->name != NULL; xattr++) {
		err = ext4_xattr_set_handle(handle, inode,
					    EXT4_XATTR_INDEX_SECURITY,
					    xattr->name, xattr->value,
					    xattr->value_len, XATTR_CREATE);
		if (err < 0)
			break;
	}
	return err;
}

int
ext4_init_security(handle_t *handle, struct inode *inode, struct inode *dir,
		   const struct qstr *qstr)
{
	return security_inode_init_security(inode, dir, qstr,
					    &ext4_initxattrs, handle);
}

const struct xattr_handler ext4_xattr_security_handler = {
	.prefix	= XATTR_SECURITY_PREFIX,
	.get	= ext4_xattr_security_get,
	.set	= ext4_xattr_security_set,
};
