/*
 * LUKS - Linux Unified Key Setup v2, PBKDF2 digest handler (LUKS1 compatible)
 *
 * Copyright (C) 2015-2022 Red Hat, Inc. All rights reserved.
 * Copyright (C) 2015-2022 Milan Broz
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "luks2_internal.h"

#define LUKS_DIGESTSIZE 20 // since SHA1
#define LUKS_SALTSIZE 32
#define LUKS_MKD_ITERATIONS_MS 125

static int PBKDF2_digest_verify(struct crypt_device *cd,
	int digest,
	const char *volume_key,
	size_t volume_key_len)
{
	char checkHashBuf[64];
	json_object *jobj_digest, *jobj1;
	const char *hashSpec;
	char *mkDigest = NULL, *mkDigestSalt = NULL;
	unsigned int mkDigestIterations;
	size_t len;
	int r = -EINVAL;

	/* This can be done only for internally linked digests */
	jobj_digest = LUKS2_get_digest_jobj(crypt_get_hdr(cd, CRYPT_LUKS2), digest);
	if (!jobj_digest)
		return -EINVAL;

	if (!json_object_object_get_ex(jobj_digest, "hash", &jobj1))
		return -EINVAL;
	hashSpec = json_object_get_string(jobj1);

	if (!json_object_object_get_ex(jobj_digest, "iterations", &jobj1))
		return -EINVAL;
	mkDigestIterations = json_object_get_int64(jobj1);

	if (!json_object_object_get_ex(jobj_digest, "salt", &jobj1))
		return -EINVAL;
	r = crypt_base64_decode(&mkDigestSalt, &len, json_object_get_string(jobj1),
				json_object_get_string_len(jobj1));
	if (r < 0)
		goto out;
	if (len != LUKS_SALTSIZE)
		goto out;

	if (!json_object_object_get_ex(jobj_digest, "digest", &jobj1))
		goto out;
	r = crypt_base64_decode(&mkDigest, &len, json_object_get_string(jobj1),
				json_object_get_string_len(jobj1));
	if (r < 0)
		goto out;
	if (len < LUKS_DIGESTSIZE ||
	    len > sizeof(checkHashBuf) ||
	    (len != LUKS_DIGESTSIZE && len != (size_t)crypt_hash_size(hashSpec)))
		goto out;

	r = -EPERM;
	if (crypt_pbkdf(CRYPT_KDF_PBKDF2, hashSpec, volume_key, volume_key_len,
			mkDigestSalt, LUKS_SALTSIZE,
			checkHashBuf, len,
			mkDigestIterations, 0, 0) < 0) {
		r = -EINVAL;
	} else {
		if (crypt_backend_memeq(checkHashBuf, mkDigest, len) == 0)
			r = 0;
	}
out:
	free(mkDigest);
	free(mkDigestSalt);
	return r;
}

static int PBKDF2_digest_store(struct crypt_device *cd,
	int digest,
	const char *volume_key,
	size_t volume_key_len)
{
	json_object *jobj_digest, *jobj_digests;
	char salt[LUKS_SALTSIZE], digest_raw[128];
	int hmac_size, r;
	char *base64_str;
	struct luks2_hdr *hdr;
	struct crypt_pbkdf_limits pbkdf_limits;
	const struct crypt_pbkdf_type *pbkdf_cd;
	struct crypt_pbkdf_type pbkdf = {
		.type = CRYPT_KDF_PBKDF2,
		.time_ms = LUKS_MKD_ITERATIONS_MS,
	};

	/* Inherit hash from PBKDF setting */
	pbkdf_cd = crypt_get_pbkdf_type(cd);
	if (pbkdf_cd)
		pbkdf.hash = pbkdf_cd->hash;
	if (!pbkdf.hash)
		pbkdf.hash = DEFAULT_LUKS1_HASH;

	log_dbg(cd, "Setting PBKDF2 type key digest %d.", digest);

	r = crypt_random_get(cd, salt, LUKS_SALTSIZE, CRYPT_RND_SALT);
	if (r < 0)
		return r;

	r = crypt_pbkdf_get_limits(CRYPT_KDF_PBKDF2, &pbkdf_limits);
	if (r < 0)
		return r;

	if (crypt_get_pbkdf(cd)->flags & CRYPT_PBKDF_NO_BENCHMARK)
		pbkdf.iterations = pbkdf_limits.min_iterations;
	else {
		r = crypt_benchmark_pbkdf_internal(cd, &pbkdf, volume_key_len);
		if (r < 0)
			return r;
	}

	hmac_size = crypt_hmac_size(pbkdf.hash);
	if (hmac_size < 0 || hmac_size > (int)sizeof(digest_raw))
		return -EINVAL;

	r = crypt_pbkdf(CRYPT_KDF_PBKDF2, pbkdf.hash, volume_key, volume_key_len,
			salt, LUKS_SALTSIZE, digest_raw, hmac_size,
			pbkdf.iterations, 0, 0);
	if (r < 0)
		return r;

	jobj_digest = LUKS2_get_digest_jobj(crypt_get_hdr(cd, CRYPT_LUKS2), digest);
	jobj_digests = NULL;
	if (!jobj_digest) {
		hdr = crypt_get_hdr(cd, CRYPT_LUKS2);
		jobj_digest = json_object_new_object();
		json_object_object_get_ex(hdr->jobj, "digests", &jobj_digests);
	}

	json_object_object_add(jobj_digest, "type", json_object_new_string("pbkdf2"));
	json_object_object_add(jobj_digest, "keyslots", json_object_new_array());
	json_object_object_add(jobj_digest, "segments", json_object_new_array());
	json_object_object_add(jobj_digest, "hash", json_object_new_string(pbkdf.hash));
	json_object_object_add(jobj_digest, "iterations", json_object_new_int(pbkdf.iterations));

	r = crypt_base64_encode(&base64_str, NULL, salt, LUKS_SALTSIZE);
	if (r < 0) {
		json_object_put(jobj_digest);
		return r;
	}
	json_object_object_add(jobj_digest, "salt", json_object_new_string(base64_str));
	free(base64_str);

	r = crypt_base64_encode(&base64_str, NULL, digest_raw, hmac_size);
	if (r < 0) {
		json_object_put(jobj_digest);
		return r;
	}
	json_object_object_add(jobj_digest, "digest", json_object_new_string(base64_str));
	free(base64_str);

	if (jobj_digests)
		json_object_object_add_by_uint(jobj_digests, digest, jobj_digest);

	JSON_DBG(cd, jobj_digest, "Digest JSON:");
	return 0;
}

static int PBKDF2_digest_dump(struct crypt_device *cd, int digest)
{
	json_object *jobj_digest, *jobj1;

	/* This can be done only for internally linked digests */
	jobj_digest = LUKS2_get_digest_jobj(crypt_get_hdr(cd, CRYPT_LUKS2), digest);
	if (!jobj_digest)
		return -EINVAL;

	json_object_object_get_ex(jobj_digest, "hash", &jobj1);
	log_std(cd, "\tHash:       %s\n", json_object_get_string(jobj1));

	json_object_object_get_ex(jobj_digest, "iterations", &jobj1);
	log_std(cd, "\tIterations: %" PRIu64 "\n", json_object_get_int64(jobj1));

	json_object_object_get_ex(jobj_digest, "salt", &jobj1);
	log_std(cd, "\tSalt:       ");
	hexprint_base64(cd, jobj1, " ", "            ");

	json_object_object_get_ex(jobj_digest, "digest", &jobj1);
	log_std(cd, "\tDigest:     ");
	hexprint_base64(cd, jobj1, " ", "            ");

	return 0;
}

const digest_handler PBKDF2_digest = {
	.name   = "pbkdf2",
	.verify = PBKDF2_digest_verify,
	.store  = PBKDF2_digest_store,
	.dump   = PBKDF2_digest_dump,
};
