diff pwfile.c @ 0:a7e41e1a79c8

Initial revision
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu, 19 Jan 2017 22:39:51 +0100
parents
children 5cd0debdb7d8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pwfile.c	Thu Jan 19 22:39:51 2017 +0100
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2016 Guido Berhoerster <guido+pwm@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"
+
+#ifdef	HAVE_ERR_H
+#include <err.h>
+#endif /* HAVE_ERR_H */
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#ifdef	HAVE_SYS_TREE_H
+#include <sys/tree.h>
+#endif
+#include <unistd.h>
+
+#include "pwfile.h"
+#include "util.h"
+
+#define	MIN_ARRAY_SIZE	1024
+
+struct record_id_entry {
+	RB_ENTRY(record_id_entry) record_id_entry;
+	unsigned int	id;
+	unsigned char	uuid[PWS3_UUID_SIZE];
+};
+
+RB_HEAD(record_id_tree, record_id_entry);
+
+static int	record_id_entry_cmp(struct record_id_entry *,
+    struct record_id_entry *);
+RB_PROTOTYPE_STATIC(record_id_tree, record_id_entry, record_id_entry,
+    record_id_entry_cmp)
+RB_GENERATE_STATIC(record_id_tree, record_id_entry, record_id_entry,
+    record_id_entry_cmp)
+
+static int
+record_id_entry_cmp(struct record_id_entry *entry1,
+    struct record_id_entry *entry2)
+{
+	if (entry1->id > entry2->id) {
+		return (-1);
+	} else if (entry1->id < entry2->id) {
+		return (1);
+	}
+	return (0);
+}
+
+static int
+pws_record_cmp(const void *p1, const void *p2)
+{
+	int		retval;
+	struct pws3_record *record1 = *(struct pws3_record **)p1;
+	struct pws3_record *record2 = *(struct pws3_record **)p2;
+	struct pws3_field *group_field1;
+	const char	*group1;
+	struct pws3_field *group_field2;
+	const char	*group2;
+	struct pws3_field *title_field1;
+	const char	*title1;
+	struct pws3_field *title_field2;
+	const char	*title2;
+
+	group_field1 = pws3_record_get_field(record1, PWS3_RECORD_FIELD_GROUP);
+	group1 = (group_field1 != NULL) ?  pws3_field_get_text(group_field1) :
+	    "";
+	group_field2 = pws3_record_get_field(record2, PWS3_RECORD_FIELD_GROUP);
+	group2 = (group_field1 != NULL) ?  pws3_field_get_text(group_field2) :
+	    "";
+	retval = strcmp(group1, group2);
+	if (retval != 0) {
+		return (retval);
+	}
+
+	title_field1 = pws3_record_get_field(record1, PWS3_RECORD_FIELD_TITLE);
+	title1 = (title_field1 != NULL) ?  pws3_field_get_text(title_field1) :
+	    "";
+	title_field2 = pws3_record_get_field(record2, PWS3_RECORD_FIELD_TITLE);
+	title2 = (title_field2 != NULL) ?  pws3_field_get_text(title_field2) :
+	    "";
+	return (strcmp(title1, title2));
+}
+
+static void
+record_id_tree_clear(struct record_id_tree *tree)
+{
+	struct record_id_entry *entry;
+	struct record_id_entry *entry_tmp;
+
+	RB_FOREACH_SAFE(entry, record_id_tree, tree, entry_tmp) {
+		RB_REMOVE(record_id_tree, tree, entry);
+		free(entry);
+	}
+}
+
+static void
+record_id_tree_destroy(struct record_id_tree *tree)
+{
+	if (tree == NULL) {
+		return;
+	}
+
+	record_id_tree_clear(tree);
+	free(tree);
+}
+
+static const unsigned char *
+record_id_tree_get_uuid(struct record_id_tree *tree, unsigned int id)
+{
+	struct record_id_entry *entry;
+
+	entry = RB_FIND(record_id_tree, tree,
+	    &(struct record_id_entry){ .id = id });
+	if (entry == NULL) {
+		return (NULL);
+	}
+
+	return (entry->uuid);
+}
+
+void
+pwfile_init(struct pwm_ctx *ctx)
+{
+	ctx->file = pws3_file_create();
+	if (ctx->file == NULL) {
+		err(1, "pws3_file_create");
+	}
+
+	ctx->next_id = 1;
+
+	ctx->record_id_tree = xmalloc(sizeof (struct record_id_tree));
+	RB_INIT(ctx->record_id_tree);
+}
+
+void
+pwfile_destroy(struct pwm_ctx *ctx)
+{
+	record_id_tree_destroy(ctx->record_id_tree);
+	ctx->record_id_tree = NULL;
+	pws3_file_destroy(ctx->file);
+	ctx->file = NULL;
+}
+
+int
+pwfile_read_file(struct pwm_ctx *ctx, FILE *fp)
+{
+	struct pws3_record *pws3_record;
+	struct pws3_record **pws3_record_list;
+	size_t		record_list_size = MIN_ARRAY_SIZE;
+	size_t		record_list_len = 0;
+	size_t		i;
+	struct pws3_field *uuid_field;
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	if (pws3_file_read_stream(ctx->file, ctx->password, fp) != 0) {
+		warnx("failed to read password database: %s",
+		    pws3_file_get_error_message(ctx->file));
+		return (-1);
+	}
+
+	record_id_tree_clear(ctx->record_id_tree);
+
+	/* sort records by group and title */
+	pws3_record_list = xmalloc(sizeof (struct pws3_record *) *
+	    record_list_size);
+	for (pws3_record = pws3_file_first_record(ctx->file);
+	    pws3_record != NULL; pws3_record = pws3_file_next_record(ctx->file,
+	    pws3_record)) {
+		if (record_list_len == record_list_size) {
+			record_list_size *= 2;
+			pws3_record_list = xrealloc(pws3_record_list,
+			    sizeof (struct pws3_record *) * record_list_size);
+		}
+		pws3_record_list[record_list_len++] = pws3_record;
+	}
+	qsort(pws3_record_list, record_list_len, sizeof (struct pws3_record *),
+	    pws_record_cmp);
+
+	/* build the tree of record IDs */
+	for (i = 0; i < record_list_len; i++) {
+		uuid_field = pws3_record_get_field(pws3_record_list[i],
+		    PWS3_RECORD_FIELD_UUID);
+		uuid = pws3_field_get_uuid(uuid_field);
+
+		entry = xmalloc(sizeof (struct record_id_entry));
+		entry->id = ctx->next_id++;
+		memcpy(entry->uuid, uuid, sizeof (entry->uuid));
+
+		RB_INSERT(record_id_tree, ctx->record_id_tree, entry);
+	}
+
+	free(pws3_record_list);
+
+	return (0);
+}
+
+static int
+make_backup_copy(const char *filename)
+{
+	int		retval = -1;
+	FILE		*fp_orig = NULL;
+	char		*backup_filename = NULL;
+	char		*tmpfilename = NULL;
+	mode_t		old_mode;
+	int		fd_backup = -1;
+	unsigned char	buf[BUFSIZ];
+	size_t		read_len;
+	FILE		*fp_backup = NULL;
+
+	fp_orig = fopen(filename, "r");
+	if (fp_orig == NULL) {
+		if (errno != ENOENT) {
+			warn("fopen");
+			return (-1);
+		}
+		return (0);
+	}
+
+	xasprintf(&backup_filename, "%s~", filename);
+	xasprintf(&tmpfilename, "%s.XXXXXX", filename);
+
+	/* create temporary file */
+	old_mode = umask(S_IRWXG | S_IRWXO);
+	fd_backup = mkstemp(tmpfilename);
+	umask(old_mode);
+	if (fd_backup == -1) {
+		warn("mkstemp");
+		goto out;
+	}
+	fp_backup = fdopen(fd_backup, "w");
+	if (fp_backup == NULL) {
+		warn("fdopen");
+		goto out;
+	}
+
+	/* copy file contents */
+	while (!feof(fp_orig)) {
+		read_len = fread(buf, 1, sizeof (buf), fp_orig);
+		if ((read_len < sizeof (buf)) && ferror(fp_orig)) {
+			warn("fread");
+			goto out;
+		}
+		if (fwrite(buf, 1, read_len, fp_backup) != read_len) {
+			warn("fwrite");
+			goto out;
+		}
+	}
+	if (fflush(fp_backup) != 0) {
+		warn("fflush");
+		goto out;
+	}
+	if (fsync(fileno(fp_backup)) != 0) {
+		warn("fsync");
+		goto out;
+	}
+
+	retval = 0;
+
+out:
+	if ((fd_backup != -1) && (fp_backup == NULL)) {
+		close(fd_backup);
+	}
+	if (fp_backup != NULL) {
+		fclose(fp_backup);
+	}
+	if (fp_orig != NULL) {
+		fclose(fp_orig);
+	}
+	if (retval == 0) {
+		/* rename temporary file and overwrite existing file */
+		if (rename(tmpfilename, backup_filename) != 0) {
+			warn("rename");
+			retval = -1;
+		}
+	}
+	if ((retval != 0) && ((fd_backup != -1) || (fp_backup != NULL))) {
+		unlink(tmpfilename);
+	}
+	free(tmpfilename);
+	free(backup_filename);
+
+	return (retval);
+}
+
+int
+pwfile_write_file(struct pwm_ctx *ctx)
+{
+	int	retval = -1;
+	char	*tmpfilename = NULL;
+	mode_t	old_mode;
+	int	fd = -1;
+	FILE	*fp = NULL;
+
+	if (make_backup_copy(ctx->filename) != 0) {
+		goto out;
+	}
+
+	xasprintf(&tmpfilename, "%s.XXXXXX", ctx->filename);
+
+	/* create temporary file */
+	old_mode = umask(S_IRWXG | S_IRWXO);
+	fd = mkstemp(tmpfilename);
+	if (fd == -1) {
+		warn("mkstemp");
+		goto out;
+	}
+	umask(old_mode);
+	fp = fdopen(fd, "w");
+	if (fp == NULL) {
+		warn("fdopen");
+		goto out;
+	}
+
+	/* write contents */
+	if (pws3_file_write_stream(ctx->file, ctx->password, 10000, fp) != 0) {
+		warnx("pws3_file_write_stream: %s",
+		    pws3_file_get_error_message(ctx->file));
+		goto out;
+	}
+	if (fflush(fp) != 0) {
+		warn("fflush");
+		goto out;
+	}
+	if (fsync(fileno(fp)) != 0) {
+		warn("fsync");
+		goto out;
+	}
+
+	retval = 0;
+
+out:
+	if ((fd != -1) && (fp == NULL)) {
+		close(fd);
+	}
+	if (fp != NULL) {
+		fclose(fp);
+	}
+	if (retval == 0) {
+		/* rename temporary file and overwrite existing file */
+		if (rename(tmpfilename, ctx->filename) != 0) {
+			warn("rename");
+			retval = -1;
+		}
+	}
+	if ((retval != 0) && ((fd != -1) || (fp != NULL))) {
+		unlink(tmpfilename);
+	}
+	free(tmpfilename);
+
+	return (retval);
+}
+
+static int
+list_item_cmp(const void *p1, const void *p2)
+{
+	int		retval;
+	const union list_item *item1 = *(const union list_item **)p1;
+	const union list_item *item2 = *(const union list_item **)p2;
+	const char	*group1;
+	const char	*group2;
+	const char	*title1;
+	const char	*title2;
+
+	/* sort both groups and records first by group name */
+	group1 = (item1->any.group != NULL) ? item1->any.group : "";
+	group2 = (item2->any.group != NULL) ? item2->any.group : "";
+	retval = strcmp(group1, group2);
+	if ((retval != 0) || ((item1->any.type == ITEM_TYPE_GROUP) &&
+	    (item2->any.type == ITEM_TYPE_GROUP))) {
+		return (retval);
+	} else if ((item1->any.type == ITEM_TYPE_GROUP) &&
+	    (item2->any.type == ITEM_TYPE_RECORD)) {
+		/* groups come before records belonging to it */
+		return (-1);
+	} else if ((item1->any.type == ITEM_TYPE_RECORD) &&
+	    (item2->any.type == ITEM_TYPE_GROUP)) {
+		return (1);
+	}
+
+	/* sort records also by title */
+	title1 = (item1->record.title != NULL) ?  item1->record.title : "";
+	title2 = (item2->record.title != NULL) ?  item2->record.title : "";
+	return (strcmp(title1, title2));
+}
+
+union list_item **
+pwfile_create_list(struct pwm_ctx *ctx)
+{
+	union list_item	**list;
+	size_t		list_size = MIN_ARRAY_SIZE;
+	size_t		list_len = 0;
+	struct record_id_entry *entry;
+	union list_item	*item;
+	struct pws3_record *pws3_record;
+	struct pws3_field *group_field;
+	const char	*group;
+	struct pws3_field *title_field;
+	const char	*title;
+	size_t		i;
+	size_t		records_len;
+	const char	*prev_group = "";
+	struct pws3_field *empty_group_field;
+
+	list = xmalloc(sizeof (union list_item *) * list_size);
+	list[0] = NULL;
+
+	/* build list of records and sort it by group and title */
+	RB_FOREACH(entry, record_id_tree, ctx->record_id_tree) {
+		if (list_len == list_size - 1) {
+			list_size *= 2;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		pws3_record = pws3_file_get_record(ctx->file, entry->uuid);
+		group_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_GROUP);
+		group = (group_field != NULL) ?
+		    pws3_field_get_text(group_field) : NULL;
+		title_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_TITLE);
+		title = (title_field != NULL) ?
+		    pws3_field_get_text(title_field) : NULL;
+
+		item = xmalloc(sizeof (union list_item));
+		item->record.type = ITEM_TYPE_RECORD;
+		item->record.group = (group != NULL) ? xstrdup(group) : NULL;
+		item->record.title = (title != NULL) ? xstrdup(title) : NULL;
+		item->record.id = entry->id;
+		memcpy(item->record.uuid, entry->uuid,
+		    sizeof (item->record.uuid));
+
+		list[list_len++] = item;
+		list[list_len] = NULL;
+	}
+	qsort(list, list_len, sizeof (union list_item *), list_item_cmp);
+
+	/* build list of groups by comparing the groups of the sorted records */
+	for (i = 0, records_len = list_len; i < records_len; i++) {
+		if (list_len == list_size - 1) {
+			list_size *= 1.5;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		group = (list[i]->record.group != NULL) ?
+		    list[i]->record.group : "";
+		if (strcmp(prev_group, group) != 0) {
+			item = xmalloc(sizeof (union list_item));
+			item->record.type = ITEM_TYPE_GROUP;
+			item->record.group = (group != NULL) ? xstrdup(group) :
+			    NULL;
+
+			list[list_len++] = item;
+			list[list_len] = NULL;
+
+			prev_group = group;
+		}
+	}
+
+	/* add empty groups to the list */
+	for (empty_group_field = pws3_file_first_empty_group(ctx->file);
+	    empty_group_field != NULL;
+	    empty_group_field = pws3_file_next_empty_group(ctx->file,
+	    empty_group_field)) {
+		if (list_len == list_size - 1) {
+			list_size *= 1.5;
+			list = xrealloc(list, sizeof (union list_item *) *
+			    list_size);
+		}
+
+		group = pws3_field_get_text(empty_group_field);
+
+		item = xmalloc(sizeof (union list_item));
+		item->record.type = ITEM_TYPE_GROUP;
+		item->record.group = xstrdup(group);
+
+		list[list_len++] = item;
+		list[list_len] = NULL;
+	}
+
+	list_size = list_len + 2;
+	list = xrealloc(list, sizeof (union list_item *) * list_size);
+	/* sort the final list by group and title */
+	qsort(list, list_len, sizeof (union list_item *), list_item_cmp);
+
+	return (list);
+}
+
+void
+pwfile_destroy_list(union list_item **list)
+{
+	size_t	i;
+
+	if (list == NULL) {
+		return;
+	}
+
+	for (i = 0; list[i] != NULL; i++) {
+		if (list[i]->any.type == ITEM_TYPE_RECORD) {
+			free(list[i]->record.title);
+		}
+		free(list[i]->any.group);
+		free(list[i]);
+	}
+
+	free(list);
+}
+
+static void
+update_record(struct pws3_record *pws3_record, struct record *record)
+{
+	struct pws3_field *title_field;
+	struct pws3_field *group_field;
+	struct pws3_field *username_field;
+	struct pws3_field *password_field;
+	struct pws3_field *notes_field;
+	struct pws3_field *url_field;
+
+	if (record->title != NULL) {
+		title_field = pws3_field_create(0, PWS3_RECORD_FIELD_TITLE);
+		if (title_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(title_field,
+		    record->title) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, title_field);
+	}
+	if (record->group != NULL) {
+		group_field = pws3_field_create(0, PWS3_RECORD_FIELD_GROUP);
+		if (group_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(group_field,
+		    record->group) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, group_field);
+	}
+	if (record->username != NULL) {
+		username_field = pws3_field_create(0,
+		    PWS3_RECORD_FIELD_USERNAME);
+		if (username_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(username_field,
+		    record->username) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, username_field);
+	}
+	if (record->password != NULL) {
+		password_field = pws3_field_create(0,
+		    PWS3_RECORD_FIELD_PASSWORD);
+		if (password_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(password_field,
+		    record->password) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, password_field);
+	}
+	if (record->notes != NULL) {
+		notes_field = pws3_field_create(0, PWS3_RECORD_FIELD_NOTES);
+		if (notes_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(notes_field, record->notes) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, notes_field);
+	}
+	if (record->url != NULL) {
+		url_field = pws3_field_create(0, PWS3_RECORD_FIELD_URL);
+		if (url_field == NULL) {
+			err(1, "pws3_record_field_create");
+		}
+		if (pws3_field_set_text(url_field, record->url) != 0) {
+			err(1, "pws3_field_set_text");
+		}
+		pws3_record_set_field(pws3_record, url_field);
+	}
+}
+
+int
+pwfile_create_record(struct pwm_ctx *ctx, struct record *record)
+{
+	struct pws3_record *pws3_record;
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	pws3_record = pws3_record_create();
+	if (pws3_record == NULL) {
+		err(1, "pws3_record_create");
+	}
+	update_record(pws3_record, record);
+	pws3_file_insert_record(ctx->file, pws3_record);
+
+	uuid = pws3_field_get_uuid(pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_UUID));
+	entry = xmalloc(sizeof (struct record_id_entry));
+	entry->id = ctx->next_id++;
+	memcpy(entry->uuid, uuid, sizeof (entry->uuid));
+	RB_INSERT(record_id_tree, ctx->record_id_tree, entry);
+
+	return (0);
+}
+
+int
+pwfile_modify_record(struct pwm_ctx *ctx, unsigned int id,
+    struct record *record)
+{
+	const unsigned char *uuid;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (-1);
+	}
+
+	update_record(pws3_file_get_record(ctx->file, uuid), record);
+
+	return (0);
+}
+
+int
+pwfile_remove_record(struct pwm_ctx *ctx, unsigned int id)
+{
+	const unsigned char *uuid;
+	struct record_id_entry *entry;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (-1);
+	}
+
+	pws3_record_destroy(pws3_file_remove_record(ctx->file, uuid));
+
+	entry = RB_FIND(record_id_tree, ctx->record_id_tree,
+	    &(struct record_id_entry){ .id = id });
+	free(RB_REMOVE(record_id_tree, ctx->record_id_tree, entry));
+
+	return (0);
+}
+
+int
+pwfile_create_group(struct pwm_ctx *ctx, const char *group)
+{
+	struct pws3_record *pws3_record;
+	struct pws3_field *group_field;
+	struct pws3_field *empty_group_field;
+
+	/* check for a record in the given group */
+	for (pws3_record = pws3_file_first_record(ctx->file);
+	    pws3_record != NULL; pws3_record = pws3_file_next_record(ctx->file,
+	    pws3_record)) {
+		group_field = pws3_record_get_field(pws3_record,
+		    PWS3_RECORD_FIELD_GROUP);
+		if ((group_field != NULL) &&
+		    (strcmp(group, pws3_field_get_text(group_field)) == 0)) {
+			return (-1);
+		}
+	}
+
+	empty_group_field = pws3_field_create(1,
+	    PWS3_HEADER_FIELD_EMPTY_GROUPS);
+	if (empty_group_field == NULL) {
+		err(1, "pws3_field_create");
+	}
+	if (pws3_field_set_text(empty_group_field, group) != 0) {
+		err(1, "pws3_field_set_text");
+	}
+	pws3_file_insert_empty_group(ctx->file, empty_group_field);
+
+	return (0);
+}
+
+int
+pwfile_remove_group(struct pwm_ctx *ctx, const char *group)
+{
+	struct pws3_field *empty_group_field;
+
+	empty_group_field = pws3_file_remove_empty_group(ctx->file, group);
+	if (empty_group_field != NULL) {
+		return (-1);
+	}
+	pws3_field_destroy(empty_group_field);
+
+	return (0);
+}
+
+struct record *
+pwfile_get_record(struct pwm_ctx *ctx, unsigned int id)
+{
+	struct record	*record;
+	const unsigned char *uuid;
+	struct pws3_record *pws3_record;
+	struct pws3_field *title_field;
+	struct pws3_field *group_field;
+	struct pws3_field *username_field;
+	struct pws3_field *password_field;
+	struct pws3_field *notes_field;
+	struct pws3_field *url_field;
+
+	uuid = record_id_tree_get_uuid(ctx->record_id_tree, id);
+	if (uuid == NULL) {
+		return (NULL);
+	}
+	pws3_record = pws3_file_get_record(ctx->file, uuid);
+
+	record = xmalloc(sizeof (struct record));
+
+	title_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_TITLE);
+	record->title = (title_field != NULL) ?
+	    xstrdup(pws3_field_get_text(title_field)) : NULL;
+
+	group_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_GROUP);
+	record->group = (group_field != NULL) ?
+	    xstrdup(pws3_field_get_text(group_field)) : NULL;
+
+	username_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_USERNAME);
+	record->username = (username_field != NULL) ?
+	    xstrdup(pws3_field_get_text(username_field)) : NULL;
+
+	password_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_PASSWORD);
+	record->password = (password_field != NULL) ?
+	    xstrdup(pws3_field_get_text(password_field)) : NULL;
+
+	notes_field = pws3_record_get_field(pws3_record,
+	    PWS3_RECORD_FIELD_NOTES);
+	record->notes = (notes_field != NULL) ?
+	    xstrdup(pws3_field_get_text(notes_field)) : NULL;
+
+	url_field = pws3_record_get_field(pws3_record, PWS3_RECORD_FIELD_URL);
+	record->url = (url_field != NULL) ?
+	    xstrdup(pws3_field_get_text(url_field)) : NULL;
+
+	return (record);
+}
+
+void
+pwfile_destroy_record(struct record *record)
+{
+	if (record == NULL) {
+		return;
+	}
+
+	free(record->title);
+	free(record->group);
+	free(record->username);
+	free(record->password);
+	free(record->notes);
+	free(record->url);
+	free(record);
+}