view pws-file.c @ 0:d541e748cfd8

Initial revision
author Guido Berhoerster <guido+libpws@berhoerster.name>
date Tue, 10 Feb 2015 11:29:54 +0100
parents
children ec5c1b653ee6
line wrap: on
line source

/*
 * Copyright (C) 2015 Guido Berhoerster <guido+libpws@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "compat.h"

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>
#include <errno.h>
#ifdef	HAVE_ENDIAN_H
#include <endian.h>
#endif /* HAVE_ENDIAN_H */
#ifdef	HAVE_SYS_ENDIAN_H
#include <sys/endian.h>
#endif /* HAVE_ENDIAN_H */
#include <nettle/twofish.h>
#include <nettle/cbc.h>
#include <nettle/hmac.h>
#include <nettle/sha.h>

#include "pws-internal.h"

#define	MAX_ITER		(1 << 22)
#define	DEFAULT_ITER		10000
#define	KEY_SIZE		32UL
#define	SALT_SIZE		32UL
#define	METADATA_SIZE		(sizeof (psafe3_tag) + SALT_SIZE + 4 +\
    SHA256_DIGEST_SIZE + KEY_SIZE + KEY_SIZE + TWOFISH_BLOCK_SIZE)

static const unsigned char psafe3_tag[] = { 'P', 'W', 'S', '3' };
static const unsigned char eof_marker[] = { 'P', 'W', 'S', '3', '-', 'E', 'O',
    'F', 'P', 'W', 'S', '3', '-', 'E', 'O', 'F' };

RB_HEAD(empty_groups_tree, pws3_field);

RB_HEAD(records_tree, pws3_record);

struct pws3_file {
	struct pws3_field	*fields[256];
	struct empty_groups_tree *empty_groups_tree;
	struct records_tree	*records_tree;
	struct pws_file_error {
		enum pws_error_code code;
		int		errnum;
		char		*msg;
	} error;
};

struct twofish_cbc_ctx CBC_CTX(struct twofish_ctx, TWOFISH_BLOCK_SIZE);

struct pws_file_ctx {
	FILE		*fp;
	unsigned char	*mem;
	size_t		mem_size;
	size_t		mem_pos;
	struct twofish_cbc_ctx cipher_ctx;
	struct hmac_sha256_ctx hmac_ctx;
	struct pws3_file *pws_file;
	uint32_t	n_iter;
};

static int	empty_groups_cmp(struct pws3_field *, struct pws3_field *);
static int	record_cmp(struct pws3_record *, struct pws3_record *);
RB_PROTOTYPE_STATIC(empty_groups_tree, pws3_field, tree_entry, empty_groups_cmp)
RB_PROTOTYPE_STATIC(records_tree, pws3_record, tree_entry, record_cmp)

RB_GENERATE_STATIC(empty_groups_tree, pws3_field, tree_entry, empty_groups_cmp)

RB_GENERATE_STATIC(records_tree, pws3_record, tree_entry, record_cmp)

static int
empty_groups_cmp(struct pws3_field *field1, struct pws3_field *field2)
{
	PWS_ASSERT(pws3_field_is_header(field1) &&
	    pws3_field_is_header(field2));
	PWS_ASSERT((pws3_field_get_type(field1) ==
	    PWS3_HEADER_FIELD_EMPTY_GROUPS) &&
	    (pws3_field_get_type(field2) == PWS3_HEADER_FIELD_EMPTY_GROUPS));

	return (strcmp(pws3_field_get_text(field1),
	    pws3_field_get_text(field2)));
}

static int
record_cmp(struct pws3_record *record1, struct pws3_record *record2)
{
	struct pws3_field	*uuid_field1;
	struct pws3_field	*uuid_field2;

	uuid_field1 = pws3_record_get_field(record1, PWS3_RECORD_FIELD_UUID);
	uuid_field2 = pws3_record_get_field(record2, PWS3_RECORD_FIELD_UUID);
	PWS_ASSERT((uuid_field1 != NULL) && (uuid_field2 != NULL));

	return (memcmp(pws3_field_get_uuid(uuid_field1),
	    pws3_field_get_uuid(uuid_field2), PWS3_UUID_SIZE));
}

static void
pws_set_system_error(struct pws3_file *pws_file, enum pws_error_code code,
    int errnum, const char *fmt, ...)
{
	char	system_error_buf[4096] = "";
	size_t	system_error_len;
	int	error_len;
	va_list	args;
	va_list	args2;

	pws_file->error.code = code;
	pws_file->error.errnum = errnum;

	strerror_r(errnum, system_error_buf, sizeof (system_error_buf) - 1);
	system_error_len = strlen(system_error_buf);

	pws_free(pws_file->error.msg, (pws_file->error.msg != NULL) ?
	    strlen(pws_file->error.msg) + 1 : 0);
	if (fmt != NULL) {
		va_start(args, fmt);
		va_copy(args2, args);
		error_len = vsnprintf(NULL, 0, fmt, args);
		pws_file->error.msg = pws_alloc(error_len + 2 +
		    system_error_len + 1);
		if (pws_file->error.msg == NULL) {
			va_end(args2);
			va_end(args);
			return;
		}
		vsnprintf(pws_file->error.msg, error_len + 1, fmt, args2);
		va_end(args2);
		va_end(args);
		strcpy(pws_file->error.msg + error_len, ": ");
		strcpy(pws_file->error.msg + error_len + 2, system_error_buf);
	} else {
		pws_file->error.msg = pws_alloc(system_error_len + 1);
		snprintf(pws_file->error.msg, system_error_len + 1, "%s",
		    system_error_buf);
	}
}

static void
pws_set_error(struct pws3_file *pws_file, enum pws_error_code code,
    const char *fmt, ...)
{
	va_list	args;
	va_list	args2;
	int	error_len;

	pws_file->error.code = code;
	pws_file->error.errnum = 0;

	pws_free(pws_file->error.msg, (pws_file->error.msg != NULL) ?
	    strlen(pws_file->error.msg) + 1 : 0);
	va_start(args, fmt);
	va_copy(args2, args);
	error_len = vsnprintf(NULL, 0, fmt, args);
	pws_file->error.msg = pws_alloc(error_len + 1);
	if (pws_file->error.msg == NULL) {
		va_end(args2);
		va_end(args);
		return;
	}
	vsnprintf(pws_file->error.msg, error_len + 1, fmt, args2);
	va_end(args2);
	va_end(args);
}

static int
read_buf(struct pws_file_ctx *ctx, unsigned char *buf, size_t buf_size)
{
	if (ctx->fp != NULL) {
		if (fread(buf, 1, buf_size, ctx->fp) != buf_size) {
			if (ferror(ctx->fp) != 0) {
				return (-1);
			} else if (feof(ctx->fp) != 0) {
				return (1);
			}
		}
	} else {
		PWS_ASSERT(ctx->mem != NULL);
		if (ctx->mem_size - ctx->mem_pos < buf_size) {
			return (1);
		}
		memcpy(buf, &ctx->mem[ctx->mem_pos], buf_size);
		ctx->mem_pos += buf_size;
	}

	return (0);
}

static int
write_buf(struct pws_file_ctx *ctx, const unsigned char *buf, size_t buf_size)
{
	size_t		remaining;
	unsigned char	*tmp;

	if (ctx->fp != NULL) {
		if (fwrite(buf, 1, buf_size, ctx->fp) != buf_size) {
			return (-1);
		}
		if (fflush(ctx->fp) != 0) {
			return (-1);
		}
	} else {
		remaining = ctx->mem_size - ctx->mem_pos;
		if (remaining < buf_size) {
			tmp = pws_realloc(ctx->mem, ctx->mem_size +
			    (buf_size - remaining));
			if (tmp == NULL) {
				return (-1);
			}
			ctx->mem = tmp;
			ctx->mem_size += (buf_size - remaining);
		}
		memcpy(&ctx->mem[ctx->mem_pos], buf, buf_size);
		ctx->mem_pos += buf_size;
	}

	return (0);
}

static void
pws_file_clear(struct pws3_file *pws_file)
{
	size_t			i;
	struct pws3_field	*empty_group_field;
	struct pws3_field	*empty_group_field_tmp;
	struct pws3_record	*record;
	struct pws3_record	*record_tmp;

	for (i = 0x00; i <= 0xff; i++) {
		pws3_field_destroy(pws_file->fields[i]);
		pws_file->fields[i] = NULL;
	}

	RB_FOREACH_SAFE(empty_group_field, empty_groups_tree,
	    pws_file->empty_groups_tree, empty_group_field_tmp) {
		pws3_field_destroy(RB_REMOVE(empty_groups_tree,
		    pws_file->empty_groups_tree, empty_group_field));
	}

	RB_FOREACH_SAFE(record, records_tree, pws_file->records_tree,
	    record_tmp) {
		pws3_record_destroy(RB_REMOVE(records_tree,
		    pws_file->records_tree, record));
	}
}

void
pws3_file_destroy(struct pws3_file *pws_file)
{
	if (pws_file == NULL) {
		return;
	}

	pws_free(pws_file->error.msg, (pws_file->error.msg != NULL) ?
	    strlen(pws_file->error.msg) + 1 : 0);
	pws_file_clear(pws_file);
	pws_free(pws_file->empty_groups_tree,
	    sizeof (struct empty_groups_tree));
	pws_free(pws_file->records_tree, sizeof (struct records_tree));
	pws_free(pws_file, sizeof (struct pws3_file));
}

struct pws3_file *
pws3_file_create(void)
{
	struct pws3_field	*version_field = NULL;
	struct pws3_file	*pws_file = NULL;
	size_t			i;

	/* version field is mandatory */
	version_field = pws3_field_create(1, PWS3_HEADER_FIELD_VERSION);
	if (version_field == NULL) {
		goto err;
	}
	pws3_field_set_uint16(version_field, PWS3_VERSION);

	pws_file = pws_alloc(sizeof (struct pws3_file));
	if (pws_file == NULL) {
		goto err;
	}
	for (i = 0x00; i <= 0xff; i++) {
		pws_file->fields[i] = NULL;
	}
	pws_file->empty_groups_tree = NULL;
	pws_file->records_tree = NULL;
	pws_file->error.errnum = 0;
	pws_file->error.code = 0;
	pws_file->error.msg = NULL;

	pws_file->empty_groups_tree =
	    pws_alloc(sizeof (struct empty_groups_tree));
	if (pws_file->empty_groups_tree == NULL) {
		goto err;
	}
	RB_INIT(pws_file->empty_groups_tree);

	pws_file->records_tree = pws_alloc(sizeof (struct records_tree));
	if (pws_file->records_tree == NULL) {
		goto err;
	}
	RB_INIT(pws_file->records_tree);

	pws3_file_set_header_field(pws_file, version_field);

	return (pws_file);
err:
	pws3_field_destroy(version_field);
	if (pws_file != NULL) {
		pws_free(pws_file->records_tree, sizeof (struct records_tree));
		pws_free(pws_file->empty_groups_tree,
		    sizeof (struct empty_groups_tree));
	}
	pws_free(pws_file, sizeof (struct pws3_file));

	return (NULL);
}

enum pws_error_code
pws3_file_get_error_code(struct pws3_file *pws_file)
{
	return (pws_file->error.code);
}

const char *
pws3_file_get_error_message(struct pws3_file *pws_file)
{
	return ((pws_file->error.msg != NULL) ? pws_file->error.msg : "");
}

static void
stretch_key(unsigned char *stretched_key, uint32_t n_iter, const char *key,
    size_t key_size, const unsigned char *salt, size_t salt_size)
{
	uint32_t	i;
	struct sha256_ctx md_ctx;

	sha256_init(&md_ctx);
	sha256_update(&md_ctx, key_size, (uint8_t *)key);
	sha256_update(&md_ctx, salt_size, salt);
	sha256_digest(&md_ctx, SHA256_DIGEST_SIZE, stretched_key);

	for (i = 0; i < n_iter; i++) {
		sha256_update(&md_ctx, SHA256_DIGEST_SIZE, stretched_key);
		sha256_digest(&md_ctx, SHA256_DIGEST_SIZE, stretched_key);
	}
}

static int
read_metadata(struct pws_file_ctx *ctx, const char *password)
{
	int		retval = -1;
	unsigned char	buf[METADATA_SIZE];
	unsigned char	*p = buf;
	unsigned char	*stretched_key = NULL;
	unsigned char	*key_k = NULL;
	unsigned char	*key_l = NULL;
	int		read_retval;
	unsigned char	salt[SALT_SIZE];
	unsigned char	key_digest[SHA256_DIGEST_SIZE];
	struct sha256_ctx md_ctx;
	struct twofish_ctx cipher_ctx;

	stretched_key = pws_secure_alloc(SHA256_DIGEST_SIZE);
	if (stretched_key == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	key_k = pws_secure_alloc(KEY_SIZE);
	if (key_k == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	key_l = pws_secure_alloc(KEY_SIZE);
	if (key_l == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	read_retval = read_buf(ctx, buf, sizeof (buf));
	if (read_retval == 1) {
		pws_set_error(ctx->pws_file, PWS_ERR_TRUNCATED_FILE,
		    "unexpected end of file");
		goto out;
	} else if (read_retval != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		goto out;
	}

	/* check tag */
	if (memcmp(p, psafe3_tag, sizeof (psafe3_tag)) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
		    "unknown filetype");
		goto out;
	}
	p += sizeof (psafe3_tag);

	/* salt */
	memcpy(salt, p, SALT_SIZE);
	p += SALT_SIZE;

	/* iterations */
	memcpy(&ctx->n_iter, p, 4);
	ctx->n_iter = le32toh(ctx->n_iter);
	if ((ctx->n_iter < 1) || (ctx->n_iter > MAX_ITER)) {
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
		    "invalid number of iterations: %d", ctx->n_iter);
		goto out;
	}
	p += 4;

	/* verify password */
	stretch_key(stretched_key, ctx->n_iter, password, strlen(password),
	    salt, SALT_SIZE);
	sha256_init(&md_ctx);
	sha256_update(&md_ctx, SHA256_DIGEST_SIZE, stretched_key);
	sha256_digest(&md_ctx, SHA256_DIGEST_SIZE, key_digest);
	if (memcmp(key_digest, p, SHA256_DIGEST_SIZE) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
		    "wrong password");
		goto out;
	}
	p += SHA256_DIGEST_SIZE;

	/* decrypt keys */
	twofish_set_key(&cipher_ctx, KEY_SIZE, stretched_key);
	twofish_decrypt(&cipher_ctx, KEY_SIZE, key_k, p);
	p += KEY_SIZE;
	twofish_decrypt(&cipher_ctx, KEY_SIZE, key_l, p);
	p += KEY_SIZE;

	/* set key for decryption */
	twofish_set_key(&ctx->cipher_ctx.ctx, KEY_SIZE, key_k);

	/* set IV */
	CBC_SET_IV(&ctx->cipher_ctx, p);

	/* set key for HMAC */
	HMAC_SET_KEY(&ctx->hmac_ctx, &nettle_sha256, KEY_SIZE, key_l);

	retval = 0;

out:
	pws_secure_free(stretched_key, (stretched_key != NULL) ?
	    SHA256_DIGEST_SIZE : 0);
	pws_secure_free(key_l, (key_l != NULL) ? KEY_SIZE : 0);
	pws_secure_free(key_k, (key_k != NULL) ? KEY_SIZE : 0);

	return (retval);
}

static int
read_block(struct pws_file_ctx *ctx, unsigned char *block)
{
	unsigned char	buf[TWOFISH_BLOCK_SIZE];
	int		read_retval;

	read_retval = read_buf(ctx, buf, sizeof (buf));
	if (read_retval == 1) {
		pws_set_error(ctx->pws_file, PWS_ERR_TRUNCATED_FILE,
		    "unexpected end of file");
		return (-1);
	} else if (read_retval != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		return (-1);
	}

	/* reached the EOF block marking the end of encrypted records */
	if (memcmp(buf, eof_marker, TWOFISH_BLOCK_SIZE) == 0) {
		return (1);
	}

	CBC_DECRYPT(&ctx->cipher_ctx, twofish_decrypt, TWOFISH_BLOCK_SIZE,
	    block, buf);

	return (0);
}

static int
read_field(struct pws_file_ctx *ctx, struct pws3_field **fieldp, int is_header)
{
	int		retval = -1;
	enum pws_data_type data_type;
	struct pws3_field *field = NULL;
	unsigned char	*block_buf = NULL;
	unsigned char	*p;
	int		read_retval;
	unsigned char	*field_buf = NULL;
	uint32_t	field_size;
	size_t		remaining;
	size_t		field_buf_size = 0;
	uint8_t field_type = 0xff;
	time_t		data_time;
	uint32_t	data_uint32;
	uint16_t	data_uint16;
	uint8_t		data_uint8;

	block_buf = pws_secure_alloc(TWOFISH_BLOCK_SIZE);
	if (block_buf == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

next_field:
	p = block_buf;

	/* read first block */
	read_retval = read_block(ctx, block_buf);
	if (read_retval != 0) {
		retval = read_retval;
		goto out;
	}

	/* determine field length */
	memcpy(&field_size, p, 4);
	remaining = field_buf_size = field_size = le32toh(field_size);
	p += 4;
	/* determine field type */
	memcpy(&field_type, p, 1);
	p++;

	/* check for end of header fields or end of record */
	if ((is_header && (field_type == PWS3_HEADER_FIELD_END)) ||
	    (!is_header && (field_type == PWS3_RECORD_FIELD_END))) {
		retval = 1;
		goto out;
	}

	/* skip empty fields */
	if (field_size == 0) {
		goto next_field;
	}

	/* determine data type */
	data_type = pws3_field_get_data_type(&(struct pws3_field){ .is_header =
	    is_header, .field_type = field_type });

	/* make room for a terminating \0 in text fields */
	if (data_type == PWS_DATA_TYPE_TEXT) {
		field_buf_size++;
	}

	/* validate field length */
	switch (data_type) {
	case PWS_DATA_TYPE_UUID:
		if ((field_size != 0) && (field_size != PWS3_UUID_SIZE)) {
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "invalid field length");
			goto out;
		}
		break;
	case PWS_DATA_TYPE_TIME:  /* FALLTHROUGH */
	case PWS_DATA_TYPE_UINT32:
		if ((field_size != 0) && (field_size != 4)) {
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "invalid field length");
			goto out;
		}
		break;
	case PWS_DATA_TYPE_UINT8:
		if ((field_size != 0) && (field_size != 1)) {
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "invalid field length");
			goto out;
		}
		break;
	case PWS_DATA_TYPE_UINT16:
		if ((field_size != 0) && (field_size != 2)) {
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "invalid field length");
			goto out;
		}
		break;
	default:
		/* text or bytes */
		if ((!is_header &&
		    (field_type == PWS3_RECORD_FIELD_PASSWORD) &&
		    (field_buf_size > PWS3_MAX_PASSWORD_LEN)) ||
		    (field_buf_size > PWS3_MAX_FIELD_SIZE)) {
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "invalid field length");
			goto out;
		}
	}

	field = pws3_field_create(is_header, field_type);
	if (field == NULL) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_NO_MEMORY, errno,
		    NULL);
		goto out;
	}

	/* create field */
	if (field_buf_size > 0) {
		if (!is_header && (field_type == PWS3_RECORD_FIELD_PASSWORD)) {
			field_buf = pws_secure_alloc(field_buf_size);
		} else {
			field_buf = pws_alloc(field_buf_size);
		}
		if (field_buf == NULL) {
			pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
			    "out of memory");
			goto out;
		}
		memset(field_buf, 0, field_buf_size);

		memcpy(field_buf, p, MIN(remaining,
		    (size_t)TWOFISH_BLOCK_SIZE - (p - block_buf)));
		remaining -= MIN(remaining,
		    (size_t)TWOFISH_BLOCK_SIZE - (p - block_buf));

		while (remaining > 0) {
			read_retval = read_block(ctx, block_buf);
			if (read_retval != 0) {
				goto out;
			}
			memcpy(field_buf + (field_size - remaining),
			    block_buf, MIN(remaining, TWOFISH_BLOCK_SIZE));
			remaining -= MIN(remaining, TWOFISH_BLOCK_SIZE);
		}

		hmac_sha256_update(&ctx->hmac_ctx, field_size, field_buf);

		switch (data_type) {
		case PWS_DATA_TYPE_UUID:
			retval = pws3_field_set_uuid(field, field_buf);
			break;
		case PWS_DATA_TYPE_TEXT:
			retval = pws3_field_set_text(field, (char *)field_buf);
			break;
		case PWS_DATA_TYPE_TIME:
			memcpy(&data_uint32, field_buf, 4);
			data_time = le32toh(data_uint32);
			retval = pws3_field_set_time(field, data_time);
			break;
		case PWS_DATA_TYPE_UINT8:
			memcpy(&data_uint8, field_buf, 1);
			retval = pws3_field_set_uint8(field, data_uint8);
			break;
		case PWS_DATA_TYPE_UINT16:
			memcpy(&data_uint16, field_buf, 2);
			data_uint16 = le16toh(data_uint16);
			retval = pws3_field_set_uint16(field, data_uint16);
			break;
		case PWS_DATA_TYPE_UINT32:
			memcpy(&data_uint32, field_buf, 4);
			data_uint32 = le32toh(data_uint32);
			retval = pws3_field_set_uint32(field, data_uint32);
			break;
		case PWS_DATA_TYPE_BYTES:
			retval = pws3_field_set_bytes(field, field_buf,
			    field_buf_size);
		}
		if (retval != 0) {
			goto out;
		}
	}

	retval = 0;

out:
	if (!is_header && (field_type == PWS3_RECORD_FIELD_PASSWORD)) {
		pws_secure_free(field_buf, field_buf_size);
	} else {
		pws_free(field_buf, field_buf_size);
	}
	pws_secure_free(block_buf, (block_buf != NULL) ?
	    (size_t)TWOFISH_BLOCK_SIZE : 0);

	if (retval == 0) {
		*fieldp = field;
	} else {
		pws3_field_destroy(field);
	}

	return (retval);
}

static int
read_header(struct pws_file_ctx *ctx)
{
	int		retval;
	struct pws3_field *field = NULL;

	/* the header must start with a version field */
	retval = read_field(ctx, &field, 1);
	if (retval != 0) {
		/* error or end of headers */
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
		    "header does not start with a version field");
		return (-1);
	} else if (field->field_type != PWS3_HEADER_FIELD_VERSION) {
		/* header does not start with a version field */
		pws3_field_destroy(field);
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
		    "header does not start with a version field");
		return (-1);
	} else if (field->value.uint16 > PWS3_VERSION) {
		/* unsupported database version */
		pws3_field_destroy(field);
		pws_set_error(ctx->pws_file, PWS_ERR_UNSUPPORTED_VERSION,
		    "unsupported database version");
		return (-1);
	}
	pws3_file_set_header_field(ctx->pws_file, field);

	for (;;) {
		retval = read_field(ctx, &field, 1);
		if (retval == 1) {
			/* end of headers */
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_HEADER,
			    "unexpected end of headers");
			break;
		} else if (retval != 0) {
			return (-1);
		}
		pws3_file_set_header_field(ctx->pws_file, field);
	}

	return (0);
}

static int
read_records(struct pws_file_ctx *ctx)
{
	int		retval;
	struct pws3_record *record = NULL;
	struct pws3_field *field = NULL;

	for (;;) {
		/*
		 * a record must consist of at least three fields, instead of
		 * the first field there could also be an EOF marker
		 */
		retval = read_field(ctx, &field, 0);
		if (retval == 1) {
			/* EOF marker */
			retval = 0;
			goto out;
		} else if (retval != 0) {
			/* read error */
			goto out;
		} else if (field->field_type == PWS3_RECORD_FIELD_END) {
			/* empty record */
			retval = -1;
			goto out;
		}

		record = pws3_record_create();
		if (record == NULL) {
			pws_set_system_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
			    errno, NULL);
			goto out;
		}

		pws3_record_set_field(record, field);
		field = NULL;

		/* read the remaining fileds */
		for (;;) {
			retval = read_field(ctx, &field, 0);
			if (retval == 1) {
				/* end of record */
				break;
			} else if (retval != 0) {
				/* read error */
				retval = -1;
				goto out;
			}
			pws3_record_set_field(record, field);
			field = NULL;
		}

		/* check whether UUID is not empty */
		if (pws3_record_get_field(record, PWS3_RECORD_FIELD_UUID) ==
		    NULL) {
			/* record is missing mandatory fields */
			pws_set_error(ctx->pws_file, PWS_ERR_INVALID_RECORD,
			    "record is missing mandatory fields");
			pws3_record_destroy(record);
			retval = -1;
			goto out;
		}

		pws3_file_insert_record(ctx->pws_file, record);
		record = NULL;
	}

out:
	if (retval != 0) {
		pws3_field_destroy(field);
		pws3_record_destroy(record);
	}

	return (retval);
}

static int
verify_checksum(struct pws_file_ctx *ctx)
{
	int		retval;
	unsigned char	hmac_file[SHA256_DIGEST_SIZE];
	unsigned char	hmac[SHA256_DIGEST_SIZE];

	retval = read_buf(ctx, hmac_file, sizeof (hmac_file));
	if (retval == 1) {
		pws_set_error(ctx->pws_file, PWS_ERR_TRUNCATED_FILE,
		    "unexpected end of file");
		return (-1);
	} else if (retval != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		return (-1);
	}

	hmac_sha256_digest(&ctx->hmac_ctx, sizeof (hmac), hmac);
	if (memcmp(hmac_file, hmac, sizeof (hmac_file)) != 0) {
		/* inconsistent database */
		pws_set_error(ctx->pws_file, PWS_ERR_INVALID_CHECKSUM,
		    "checksum failed");
		return (-1);
	}

	return (0);
}

static int
pws3_file_read(struct pws3_file *pws_file, const char *password,
    unsigned char *s, size_t n, FILE *fp)
{
	int		retval = -1;
	struct pws_file_ctx	ctx = {
		.fp = fp,
		.mem = s,
		.mem_size = n,
		.mem_pos = 0,
		.pws_file = pws_file,
	};

	pws_file_clear(pws_file);

	retval = read_metadata(&ctx, password);
	if (retval != 0) {
		goto out;
	}

	retval = read_header(&ctx);
	if (retval != 0) {
		goto out;
	}

	retval = read_records(&ctx);
	if (retval != 0) {
		goto out;
	}

	retval = verify_checksum(&ctx);
	if (retval != 0) {
		goto out;
	}

out:
	if (retval != 0) {
		pws_file_clear(ctx.pws_file);
	}

	return (retval);
}

int
pws3_file_read_mem(struct pws3_file *pws_file, const char *password,
    unsigned char *s, size_t n)
{
	return (pws3_file_read(pws_file, password, s, n, NULL));
}

int
pws3_file_read_stream(struct pws3_file *pws_file, const char *password,
    FILE *fp)
{
	return (pws3_file_read(pws_file, password, NULL, 0, fp));
}

static int
write_metadata(struct pws_file_ctx *ctx, const char *password)
{
	int		retval = -1;
	unsigned char	*stretched_key = NULL;
	unsigned char	*key_k = NULL;
	unsigned char	*key_l = NULL;
	unsigned char	metadata[METADATA_SIZE];
	unsigned char	*p = metadata;
	unsigned char	*salt;
	uint32_t	n_iter_le;
	struct sha256_ctx md_ctx;
	unsigned char	*b1;
	unsigned char	*b3;
	unsigned char	*iv;
	struct twofish_ctx cipher_ctx;

	stretched_key = pws_secure_alloc(SHA256_DIGEST_SIZE);
	if (stretched_key == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	key_k = pws_secure_alloc(KEY_SIZE);
	if (key_k == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	key_l = pws_secure_alloc(KEY_SIZE);
	if (key_l == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	/* generate new keys */
	if (pws_random_bytes(key_k, KEY_SIZE) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_GENERIC_ERROR,
		    "failed to generate key");
		goto out;
	}

	if (pws_random_bytes(key_l, KEY_SIZE) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_GENERIC_ERROR,
		    "failed to generate key");
		goto out;
	}

	/* tag */
	memcpy(p, psafe3_tag, sizeof (psafe3_tag));
	p += sizeof (psafe3_tag);

	/* generate new salt */
	salt = p;
	if (pws_random_bytes(salt, SALT_SIZE) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_GENERIC_ERROR,
		    "failed to generate salt");
		goto out;
	}
	p += SALT_SIZE;

	/* number of iterations */
	n_iter_le = htole32(ctx->n_iter);
	memcpy(p, &n_iter_le, 4);
	p += 4;

	/* stretch, hash password */
	stretch_key(stretched_key, ctx->n_iter, password, strlen(password),
	    salt, SALT_SIZE);
	sha256_init(&md_ctx);
	sha256_update(&md_ctx, SHA256_DIGEST_SIZE, stretched_key);
	sha256_digest(&md_ctx, SHA256_DIGEST_SIZE, p);
	p += SHA256_DIGEST_SIZE;

	b1 = p;
	p += KEY_SIZE;

	b3 = p;
	p += KEY_SIZE;

	/* generate IV */
	iv = p;
	if (pws_random_bytes(iv, TWOFISH_BLOCK_SIZE) != 0) {
		pws_set_error(ctx->pws_file, PWS_ERR_GENERIC_ERROR,
		    "failed to generate IV");
		goto out;
	}

	/* encrypt keys */
	twofish_set_key(&cipher_ctx, KEY_SIZE, stretched_key);
	twofish_encrypt(&cipher_ctx, KEY_SIZE, b1, key_k);
	twofish_encrypt(&cipher_ctx, KEY_SIZE, b3, key_l);

	/* set key for decryption */
	twofish_set_key(&ctx->cipher_ctx.ctx, KEY_SIZE, key_k);

	/* set IV */
	CBC_SET_IV(&ctx->cipher_ctx, p);

	/* set key for HMAC */
	hmac_sha256_set_key(&ctx->hmac_ctx, KEY_SIZE, key_l);

	/* write metadata */
	if (write_buf(ctx, metadata, sizeof (metadata)) != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		retval = -1;
		goto out;
	}

	retval = 0;

out:
	pws_secure_free(key_k, (key_k != NULL) ? KEY_SIZE : 0);
	pws_secure_free(key_l, (key_l != NULL) ? KEY_SIZE : 0);
	pws_secure_free(stretched_key, (stretched_key != NULL) ?
	    SHA256_DIGEST_SIZE : 0);

	return (retval);
}

static int
write_block(struct pws_file_ctx *ctx, unsigned char *block)
{
	unsigned char	buf[TWOFISH_BLOCK_SIZE];

	CBC_ENCRYPT(&ctx->cipher_ctx, twofish_encrypt, sizeof (buf), buf,
	    block);

	if (write_buf(ctx, buf, sizeof (buf)) != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		return (-1);
	}

	return (0);
}

static int
write_field(struct pws_file_ctx *ctx, struct pws3_field *field)
{
	int		retval = -1;
	unsigned char	*buf = NULL;
	unsigned char	*p;
	unsigned char	*field_data;
	enum pws_data_type data_type;
	size_t		blocks = (field->size + 4 + 1) / TWOFISH_BLOCK_SIZE +
	    ((field->size + 4 + 1) % TWOFISH_BLOCK_SIZE != 0);
	size_t		i;
	size_t		j;
	uint32_t	len_le;

	buf = pws_secure_alloc(TWOFISH_BLOCK_SIZE);
	if (buf == NULL) {
		pws_set_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
		    "out of memory");
		goto out;
	}

	data_type = pws3_field_get_data_type(field);

	for (i = 0, j = 0; i < blocks; i++) {
		p = field_data = buf;
		if (pws_random_bytes(buf, TWOFISH_BLOCK_SIZE) != 0) {
			pws_set_error(ctx->pws_file, PWS_ERR_GENERIC_ERROR,
			    "could not get random numbers");
			goto out;
		}

		/* the first block of the field contains the length and type */
		if (i == 0) {
			len_le = htole32(field->size);
			memcpy(p, &len_le, 4);
			p += 4;

			*p = field->field_type;
			p++;
			field_data = p;
		}

		while ((j < field->size) &&
		    (p - buf < (ptrdiff_t)TWOFISH_BLOCK_SIZE)) {
			switch (data_type) {
			case PWS_DATA_TYPE_UINT8:
				*p = field->value.uint8;
				break;
			case PWS_DATA_TYPE_UINT16:
				/* little endian */
				*p = (field->value.uint16 >> (8 * j)) & 0xff;
				break;
			case PWS_DATA_TYPE_TIME: /* FALLTHROUGH */
			case PWS_DATA_TYPE_UINT32:
				/* little endian */
				*p = (field->value.uint32 >> (8 * j)) & 0xff;
				break;
			case PWS_DATA_TYPE_TEXT:
				*p = field->value.text[j];
				break;
			case PWS_DATA_TYPE_UUID:
				*p = field->value.uuid[j];
				break;
			default:
				*p = field->value.bytes[j];
			}

			p++;
			j++;
		}

		hmac_sha256_update(&ctx->hmac_ctx, p - field_data, field_data);

		retval = write_block(ctx, buf);
		if (retval != 0) {
			goto out;
		}
	}

	retval = 0;

out:
	pws_secure_free(buf, (buf != NULL) ? TWOFISH_BLOCK_SIZE : 0);

	return (retval);
}

static int
write_header(struct pws_file_ctx *ctx)
{
	int		retval = -1;
	size_t		i;
	struct pws3_field *version_field;
	struct pws3_field *field = NULL;
	struct pws3_field *end_field = NULL;

	version_field = pws3_file_get_header_field(ctx->pws_file,
	    PWS3_HEADER_FIELD_VERSION);
	if (version_field == NULL) {
		/* add mandatory version header version_field if necessary */
		version_field = pws3_field_create(1, PWS3_HEADER_FIELD_VERSION);
		if (version_field == NULL) {
			pws_set_system_error(ctx->pws_file, PWS_ERR_NO_MEMORY,
			    errno, NULL);
			goto out;
		}
		pws3_field_set_uint16(version_field, PWS3_VERSION);
		pws3_file_set_header_field(ctx->pws_file, version_field);
	}
	retval = write_field(ctx, version_field);
	if (retval != 0) {
		goto out;
	}

	for (i = 0x01; i < 0xff; i++) {
		if (ctx->pws_file->fields[i] != NULL) {
			retval = write_field(ctx, ctx->pws_file->fields[i]);
			if (retval != 0) {
				goto out;
			}
		}
	}

	RB_FOREACH(field, empty_groups_tree, ctx->pws_file->empty_groups_tree) {
		retval = write_field(ctx, field);
		if (retval != 0) {
			goto out;
		}
	}

	end_field = pws3_field_create(1, PWS3_HEADER_FIELD_END);
	retval = write_field(ctx, end_field);
	if (retval != 0) {
		goto out;
	}

out:
	pws3_field_destroy(end_field);

	return (retval);
}

static int
write_records(struct pws_file_ctx *ctx)
{
	int		retval = -1;
	struct pws3_field *end_field = NULL;
	size_t		i;
	struct pws3_record *record;

	end_field = pws3_field_create(0, PWS3_RECORD_FIELD_END);
	if (end_field == NULL) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_NO_MEMORY, errno,
		    NULL);
		goto out;
	}

	RB_FOREACH(record, records_tree, ctx->pws_file->records_tree) {
		/* record fields */
		for (i = 0x01; i < 0xff; i++) {
			if (record->fields[i] != NULL) {
				retval = write_field(ctx, record->fields[i]);
				if (retval != 0) {
					goto out;
				}
			}
		}

		/* end of entry marker */
		retval = write_field(ctx, end_field);
		if (retval != 0) {
			goto out;
		}
	}

	/* end of file marker */
	if (write_buf(ctx, eof_marker, sizeof (eof_marker)) != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		retval = -1;
		goto out;
	}

	retval = 0;

out:
	pws3_field_destroy(end_field);

	return (retval);
}

static int
write_checksum(struct pws_file_ctx *ctx)
{
	unsigned char	hmac[SHA256_DIGEST_SIZE];

	hmac_sha256_digest(&ctx->hmac_ctx, sizeof (hmac), hmac);

	if (write_buf(ctx, hmac, sizeof (hmac)) != 0) {
		pws_set_system_error(ctx->pws_file, PWS_ERR_IO_ERROR, errno,
		    NULL);
		return (-1);
	}

	return (0);
}

static int
pws3_file_write(struct pws3_file *pws_file, const char *password,
    uint32_t n_iter, unsigned char **memp, size_t *mem_sizep, FILE *fp)
{
	int		retval = -1;
	struct pws_file_ctx	ctx = {
		.fp = fp,
		.pws_file = pws_file,
		.n_iter = n_iter
	};

	retval = write_metadata(&ctx, password);
	if (retval != 0) {
		goto out;
	}

	retval = write_header(&ctx);
	if (retval != 0) {
		goto out;
	}

	retval = write_records(&ctx);
	if (retval != 0) {
		goto out;
	}

	retval = write_checksum(&ctx);
	if (retval != 0) {
		goto out;
	}

	if (memp != NULL) {
		*memp = ctx.mem;
		*mem_sizep = ctx.mem_size;
	}

out:
	if (retval != 0) {
		pws_free(ctx.mem, ctx.mem_size);
	}

	return (retval);
}

int
pws3_file_write_mem(struct pws3_file *pws_file, const char *password,
    uint32_t n_iter, unsigned char **memp, size_t *mem_sizep)
{
	PWS_ASSERT(memp != NULL);
	PWS_ASSERT(mem_sizep != NULL);

	return (pws3_file_write(pws_file, password, n_iter, memp, mem_sizep,
	    NULL));
}

int
pws3_file_write_stream(struct pws3_file *pws_file, const char *password,
    uint32_t n_iter, FILE *fp)
{
	PWS_ASSERT(fp != NULL);

	return (pws3_file_write(pws_file, password, n_iter, NULL, NULL, fp));
}

void
pws3_file_set_header_field(struct pws3_file *pws_file, struct pws3_field *field)
{
	PWS_ASSERT(pws3_field_is_header(field));
	PWS_ASSERT((pws3_field_get_data_type(field) != PWS_DATA_TYPE_TEXT) ||
	    (field->value.text != NULL));
	PWS_ASSERT((pws3_field_get_data_type(field) != PWS_DATA_TYPE_BYTES) ||
	    (field->value.bytes != NULL));

	if (field->field_type == PWS3_HEADER_FIELD_EMPTY_GROUPS) {
		pws3_file_insert_empty_group(pws_file, field);
		return;
	}

	pws3_field_destroy(pws3_file_remove_header_field(pws_file,
	    field->field_type));
	pws_file->fields[field->field_type] = field;
}

struct pws3_field *
pws3_file_get_header_field(struct pws3_file *pws_file, uint8_t field_type)
{
	if (field_type == PWS3_HEADER_FIELD_EMPTY_GROUPS) {
		return (pws3_file_first_empty_group(pws_file));
	}

	return (pws_file->fields[field_type]);
}

struct pws3_field *
pws3_file_remove_header_field(struct pws3_file *pws_file, uint8_t field_type)
{
	struct pws3_field	*field;

	if (field_type == PWS3_HEADER_FIELD_EMPTY_GROUPS) {
		return (NULL);
	}

	field = pws3_file_get_header_field(pws_file, field_type);
	pws_file->fields[field_type] = NULL;

	return (field);
}

void
pws3_file_insert_empty_group(struct pws3_file *pws_file,
    struct pws3_field *field)
{
	const char	*group_name;

	PWS_ASSERT(pws3_field_is_header(field));
	PWS_ASSERT(pws3_field_get_type(field) ==
	    PWS3_HEADER_FIELD_EMPTY_GROUPS);

	group_name = pws3_field_get_text(field);
	pws3_field_destroy(pws3_file_remove_empty_group(pws_file, group_name));
	RB_INSERT(empty_groups_tree, pws_file->empty_groups_tree, field);
}

struct pws3_field *
pws3_file_get_empty_group(struct pws3_file *pws_file, const char *group_name)
{
	return (RB_FIND(empty_groups_tree, pws_file->empty_groups_tree,
	    (&(struct pws3_field){ .is_header = 1,
	    .field_type = PWS3_HEADER_FIELD_EMPTY_GROUPS,
	    .value.text = (char *)group_name })));
}

struct pws3_field *
pws3_file_remove_empty_group(struct pws3_file *pws_file, const char *group_name)
{
	struct pws3_field *field;

	field = RB_FIND(empty_groups_tree, pws_file->empty_groups_tree,
	    (&(struct pws3_field){ .is_header = 1,
	    .field_type = PWS3_HEADER_FIELD_EMPTY_GROUPS,
	    .value.text = (char *)group_name }));
	if (field != NULL) {
		RB_REMOVE(empty_groups_tree, pws_file->empty_groups_tree,
		    field);
	}

	return (field);
}

struct pws3_field *
pws3_file_first_empty_group(struct pws3_file *pws_file)
{
	return (RB_MIN(empty_groups_tree, pws_file->empty_groups_tree));
}

struct pws3_field *
pws3_file_last_empty_group(struct pws3_file *pws_file)
{
	return (RB_MAX(empty_groups_tree, pws_file->empty_groups_tree));
}

struct pws3_field *
pws3_file_next_empty_group(struct pws3_file *pws_file, struct pws3_field *field)
{
	return (RB_NEXT(empty_groups_tree, pws_file->empty_groups_tree, field));
}

struct pws3_field *
pws3_file_prev_empty_group(struct pws3_file *pws_file, struct pws3_field *field)
{
	return (RB_PREV(empty_groups_tree, pws_file->empty_groups_tree, field));
}

void
pws3_file_insert_record(struct pws3_file *pws_file, struct pws3_record *record)
{
	struct pws3_field	*uuid_field;
	const unsigned char	*uuid;

	uuid_field = pws3_record_get_field(record, PWS3_RECORD_FIELD_UUID);
	PWS_ASSERT(uuid_field != NULL);
	uuid = pws3_field_get_uuid(uuid_field);

	/* replace existing record */
	pws3_record_destroy(pws3_file_remove_record(pws_file, uuid));

	RB_INSERT(records_tree, pws_file->records_tree, record);
}

struct pws3_record *
pws3_file_get_record(struct pws3_file *pws_file,
    const unsigned char uuid[static PWS3_UUID_SIZE])
{
	struct pws3_field uuid_field = {
		.is_header = 0,
		.field_type = PWS3_RECORD_FIELD_UUID
	};
	struct pws3_record search_record = {
		.fields[PWS3_RECORD_FIELD_UUID] = &uuid_field
	};

	memcpy(uuid_field.value.uuid, uuid, PWS3_UUID_SIZE);

	return (RB_FIND(records_tree, pws_file->records_tree, &search_record));
}

struct pws3_record *
pws3_file_remove_record(struct pws3_file *pws_file,
    const unsigned char uuid[static PWS3_UUID_SIZE])
{
	struct pws3_record *record;

	record = pws3_file_get_record(pws_file, uuid);
	if (record != NULL) {
		RB_REMOVE(records_tree, pws_file->records_tree, record);
	}

	return (record);
}

struct pws3_record *
pws3_file_first_record(struct pws3_file *pws_file)
{
	return (RB_MIN(records_tree, pws_file->records_tree));
}

struct pws3_record *
pws3_file_last_record(struct pws3_file *pws_file)
{
	return (RB_MAX(records_tree, pws_file->records_tree));
}

struct pws3_record *
pws3_file_next_record(struct pws3_file *pws_file, struct pws3_record *record)
{
	return (RB_NEXT(records_tree, pws_file->records_tree, record));
}

struct pws3_record *
pws3_file_prev_record(struct pws3_file *pws_file, struct pws3_record *record)
{
	return (RB_PREV(records_tree, pws_file->records_tree, record));
}