/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 1996-2005
 *	Sleepycat Software.  All rights reserved.
 *
 * Some parts of this code originally written by Adam Stubblefield
 * -- astubble@rice.edu
 *
 * $Id: crypto.c,v 12.5 2005/07/20 16:50:56 bostic Exp $
 */

#include "db_config.h"

#ifndef NO_SYSTEM_INCLUDES
#include <string.h>
#endif

#include "db_int.h"
#include "dbinc/db_page.h"
#include "dbinc/crypto.h"

/*
 * __crypto_region_init --
 *	Initialize crypto.
 */
int
__crypto_region_init(dbenv)
	DB_ENV *dbenv;
{
	REGENV *renv;
	REGINFO *infop;
	CIPHER *cipher;
	DB_CIPHER *db_cipher;
	char *sh_passwd;
	int ret;

	db_cipher = dbenv->crypto_handle;

	ret = 0;
	infop = dbenv->reginfo;
	renv = infop->primary;
	if (renv->cipher_off == INVALID_ROFF) {
		if (!CRYPTO_ON(dbenv))
			return (0);
		if (!F_ISSET(infop, REGION_CREATE)) {
			__db_err(dbenv,
    "Joining non-encrypted environment with encryption key");
			return (EINVAL);
		}
		if (F_ISSET(db_cipher, CIPHER_ANY)) {
			__db_err(dbenv, "Encryption algorithm not supplied");
			return (EINVAL);
		}
		/*
		 * Must create the shared information.  We need: Shared cipher
		 * information that contains the passwd.  After we copy the
		 * passwd, we smash and free the one in the dbenv.
		 */
		if ((ret =
		    __db_shalloc(infop, sizeof(CIPHER), 0, &cipher)) != 0)
			return (ret);
		memset(cipher, 0, sizeof(*cipher));
		if ((ret = __db_shalloc(
		    infop, dbenv->passwd_len, 0, &sh_passwd)) != 0) {
			__db_shalloc_free(infop, cipher);
			return (ret);
		}
		memset(sh_passwd, 0, dbenv->passwd_len);
		cipher->passwd = R_OFFSET(infop, sh_passwd);
		cipher->passwd_len = dbenv->passwd_len;
		cipher->flags = db_cipher->alg;
		memcpy(sh_passwd, dbenv->passwd, cipher->passwd_len);
		renv->cipher_off = R_OFFSET(infop, cipher);
	} else {
		if (!CRYPTO_ON(dbenv)) {
			__db_err(dbenv,
		    "Encrypted environment: no encryption key supplied");
			return (EINVAL);
		}
		cipher = R_ADDR(infop, renv->cipher_off);
		sh_passwd = R_ADDR(infop, cipher->passwd);
		if ((cipher->passwd_len != dbenv->passwd_len) ||
		    memcmp(dbenv->passwd, sh_passwd, cipher->passwd_len) != 0) {
			__db_err(dbenv, "Invalid password");
			return (EPERM);
		}
		if (!F_ISSET(db_cipher, CIPHER_ANY) &&
		    db_cipher->alg != cipher->flags) {
			__db_err(dbenv,
    "Environment encrypted using a different algorithm");
			return (EINVAL);
		}
		if (F_ISSET(db_cipher, CIPHER_ANY))
			/*
			 * We have CIPHER_ANY and we are joining the existing
			 * env.  Setup our cipher structure for whatever
			 * algorithm this env has.
			 */
			if ((ret = __crypto_algsetup(dbenv, db_cipher,
			    cipher->flags, 0)) != 0)
				return (ret);
	}
	ret = db_cipher->init(dbenv, db_cipher);

	/*
	 * On success, no matter if we allocated it or are using the already
	 * existing one, we are done with the passwd in the dbenv.  We smash
	 * N-1 bytes so that we don't overwrite the nul.
	 */
	memset(dbenv->passwd, 0xff, dbenv->passwd_len-1);
	__os_free(dbenv, dbenv->passwd);
	dbenv->passwd = NULL;
	dbenv->passwd_len = 0;

	return (ret);
}

/*
 * __crypto_dbenv_close --
 *	Crypto-specific destruction of DB_ENV structure.
 *
 * PUBLIC: int __crypto_dbenv_close __P((DB_ENV *));
 */
int
__crypto_dbenv_close(dbenv)
	DB_ENV *dbenv;
{
	DB_CIPHER *db_cipher;
	int ret;

	ret = 0;
	db_cipher = dbenv->crypto_handle;
	if (dbenv->passwd != NULL) {
		memset(dbenv->passwd, 0xff, dbenv->passwd_len-1);
		__os_free(dbenv, dbenv->passwd);
		dbenv->passwd = NULL;
	}
	if (!CRYPTO_ON(dbenv))
		return (0);
	if (!F_ISSET(db_cipher, CIPHER_ANY))
		ret = db_cipher->close(dbenv, db_cipher->data);
	__os_free(dbenv, db_cipher);
	return (ret);
}

/*
 * __crypto_region_destroy --
 *	Destroy any system resources allocated in the primary region.
 *
 * PUBLIC: int __crypto_region_destroy __P((DB_ENV *));
 */
int
__crypto_region_destroy(dbenv)
	DB_ENV *dbenv;
{
	CIPHER *cipher;
	REGENV *renv;
	REGINFO *infop;

	infop = dbenv->reginfo;
	renv = infop->primary;
	if (renv->cipher_off != INVALID_ROFF) {
		cipher = R_ADDR(infop, renv->cipher_off);
		__db_shalloc_free(infop, R_ADDR(infop, cipher->passwd));
		__db_shalloc_free(infop, cipher);
	}
	return (0);
}

/*
 * __crypto_algsetup --
 *	Given a db_cipher structure and a valid algorithm flag, call
 * the specific algorithm setup function.
 *
 * PUBLIC: int __crypto_algsetup __P((DB_ENV *, DB_CIPHER *, u_int32_t, int));
 */
int
__crypto_algsetup(dbenv, db_cipher, alg, do_init)
	DB_ENV *dbenv;
	DB_CIPHER *db_cipher;
	u_int32_t alg;
	int do_init;
{
	int ret;

	ret = 0;
	if (!CRYPTO_ON(dbenv)) {
		__db_err(dbenv, "No cipher structure given");
		return (EINVAL);
	}
	F_CLR(db_cipher, CIPHER_ANY);
	switch (alg) {
	case CIPHER_AES:
		db_cipher->alg = CIPHER_AES;
		ret = __aes_setup(dbenv, db_cipher);
		break;
	default:
		__db_panic(dbenv, EINVAL);
		/* NOTREACHED */
	}
	if (do_init)
		ret = db_cipher->init(dbenv, db_cipher);
	return (ret);
}

/*
 * __crypto_decrypt_meta --
 *	Perform decryption on a metapage if needed.
 *
 * PUBLIC:  int __crypto_decrypt_meta __P((DB_ENV *, DB *, u_int8_t *, int));
 */
int
__crypto_decrypt_meta(dbenv, dbp, mbuf, do_metachk)
	DB_ENV *dbenv;
	DB *dbp;
	u_int8_t *mbuf;
	int do_metachk;
{
	DB_CIPHER *db_cipher;
	DB dummydb;
	DBMETA *meta;
	size_t pg_off;
	int ret;
	u_int8_t *iv;

	/*
	 * If we weren't given a dbp, we just want to decrypt the page on
	 * behalf of some internal subsystem, not on behalf of a user with
	 * a dbp.  Therefore, set up a dummy dbp so that the call to
	 * P_OVERHEAD below works.
	 */
	if (dbp == NULL) {
		memset(&dummydb, 0, sizeof(DB));
		dbp = &dummydb;
	}

	ret = 0;
	meta = (DBMETA *)mbuf;

	/*
	 * !!!
	 * We used an "unused" field in the meta-data page to flag whether or
	 * not the database is encrypted.  Unfortunately, that unused field
	 * was used in Berkeley DB releases before 3.0 (for example, 2.7.7).
	 * It would have been OK, except encryption doesn't follow the usual
	 * rules of "upgrade before doing anything else", we check encryption
	 * before checking for old versions of the database.
	 *
	 * We don't have to check Btree databases -- before 3.0, the field of
	 * interest was the bt_maxkey field (which was never supported and has
	 * since been removed).
	 *
	 * Ugly check to jump out if this format is older than what we support.
	 * It assumes no encrypted page will have an unencrypted magic number,
	 * but that seems relatively safe.  [#10920]
	 */
	if (meta->magic == DB_HASHMAGIC && meta->version <= 5)
		return (0);

	/*
	 * Meta-pages may be encrypted for DBMETASIZE bytes.  If we have a
	 * non-zero IV (that is written after encryption) then we decrypt (or
	 * error if the user isn't set up for security).  We guarantee that
	 * the IV space on non-encrypted pages will be zero and a zero-IV is
	 * illegal for encryption.  Therefore any non-zero IV means an
	 * encrypted database.  This basically checks the passwd on the file
	 * if we cannot find a good magic number.  We walk through all the
	 * algorithms we know about attempting to decrypt (and possibly
	 * byteswap).
	 *
	 * !!!
	 * All method meta pages have the IV and checksum at the exact same
	 * location, but not in DBMETA, use BTMETA.
	 */
	if (meta->encrypt_alg != 0) {
		db_cipher = (DB_CIPHER *)dbenv->crypto_handle;
		if (!F_ISSET(dbp, DB_AM_ENCRYPT)) {
			if (!CRYPTO_ON(dbenv)) {
				__db_err(dbenv,
    "Encrypted database: no encryption flag specified");
				return (EINVAL);
			}
			/*
			 * User has a correct, secure env, but has encountered
			 * a database in that env that is secure, but user
			 * didn't dbp->set_flags.  Since it is existing, use
			 * encryption if it is that way already.
			 */
			F_SET(dbp, DB_AM_ENCRYPT|DB_AM_CHKSUM);
		}
		/*
		 * This was checked in set_flags when DB_AM_ENCRYPT was set.
		 * So it better still be true here.
		 */
		DB_ASSERT(CRYPTO_ON(dbenv));
		if (!F_ISSET(db_cipher, CIPHER_ANY) &&
		    meta->encrypt_alg != db_cipher->alg) {
			__db_err(dbenv,
			    "Database encrypted using a different algorithm");
			return (EINVAL);
		}
		DB_ASSERT(F_ISSET(dbp, DB_AM_CHKSUM));
		iv = ((BTMETA *)mbuf)->iv;
		/*
		 * For ALL pages, we do not encrypt the beginning of the page
		 * that contains overhead information.  This is true of meta
		 * and all other pages.
		 */
		pg_off = P_OVERHEAD(dbp);
alg_retry:
		/*
		 * If they asked for a specific algorithm, then
		 * use it.  Otherwise walk through those we know.
		 */
		if (!F_ISSET(db_cipher, CIPHER_ANY)) {
			if (do_metachk && (ret = db_cipher->decrypt(dbenv,
			    db_cipher->data, iv, mbuf + pg_off,
			    DBMETASIZE - pg_off)))
				return (ret);
			if (((BTMETA *)meta)->crypto_magic !=
			    meta->magic) {
				__db_err(dbenv, "Invalid password");
				return (EINVAL);
			}
			/*
			 * Success here.  The algorithm asked for and the one
			 * on the file match.  We've just decrypted the meta
			 * page and checked the magic numbers.  They match,
			 * indicating the password is right.  All is right
			 * with the world.
			 */
			return (0);
		}
		/*
		 * If we get here, CIPHER_ANY must be set.
		 */
		ret = __crypto_algsetup(dbenv, db_cipher, meta->encrypt_alg, 1);
		goto alg_retry;
	} else if (F_ISSET(dbp, DB_AM_ENCRYPT)) {
		/*
		 * They gave us a passwd, but the database is not encrypted.
		 * This is an error.  We do NOT want to silently allow them
		 * to write data in the clear when the user set up and expects
		 * encrypted data.
		 *
		 * This covers at least the following scenario.
		 * 1.  User creates and sets up an encrypted database.
		 * 2.  Attacker cannot read the actual data in the database
		 * because it is encrypted, but can remove/replace the file
		 * with an empty, unencrypted database file.
		 * 3.  User sets encryption and we get to this code now.
		 * If we allowed the file to be used in the clear since
		 * it is that way on disk, the user would unsuspectingly
		 * write sensitive data in the clear.
		 * 4.  Attacker reads data that user thought was encrypted.
		 *
		 * Therefore, asking for encryption with a database that
		 * was not encrypted is an error.
		 */
		__db_err(dbenv,
		    "Unencrypted database with a supplied encryption key");
		return (EINVAL);
	}
	return (ret);
}

/*
 * __crypto_set_passwd --
 *	Get the password from the shared region; and set it in a new
 * environment handle.  Use this to duplicate environment handles.
 *
 * PUBLIC: int __crypto_set_passwd __P((DB_ENV *, DB_ENV *));
 */
int
__crypto_set_passwd(dbenv_src, dbenv_dest)
	DB_ENV *dbenv_src, *dbenv_dest;
{
	CIPHER *cipher;
	REGENV *renv;
	REGINFO *infop;
	char *sh_passwd;
	int ret;

	ret = 0;
	infop = dbenv_src->reginfo;
	renv = infop->primary;

	DB_ASSERT(CRYPTO_ON(dbenv_src));

	cipher = R_ADDR(infop, renv->cipher_off);
	sh_passwd = R_ADDR(infop, cipher->passwd);
	return (__env_set_encrypt(dbenv_dest, sh_passwd, DB_ENCRYPT_AES));
}
