~sircmpwn/himitsu-devel

himitsu: Implement dynamic store unlocking with the new prompter protocol v1 APPLIED

Alexey Yerin: 1
 Implement dynamic store unlocking with the new prompter protocol

 6 files changed, 135 insertions(+), 56 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/himitsu-devel/patches/33024/mbox | git am -3
Learn more about email & git

[PATCH himitsu] Implement dynamic store unlocking with the new prompter protocol Export this patch

---
 cmd/himitsud/cmd.ha   | 79 +++++++++++++++++++++++++++++++++++++------
 cmd/himitsud/main.ha  | 39 ---------------------
 prompt/prompter.ha    | 48 ++++++++++++++++++++++++--
 secstore/secstore.ha  | 11 ++++--
 secstore/serialize.ha |  2 +-
 secstore/types.ha     | 12 +++++++
 6 files changed, 135 insertions(+), 56 deletions(-)

diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index efa4383..3f160e6 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -74,15 +74,21 @@ fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = {
};

fn exec_add(serv: *server, client: *client, args: []str) (void | cmderror) = {
	if (serv.store.state != secstore::state::UNLOCKED) {
		const prompter = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
		prompt::unlock(&prompter)?;
		if (!prompt::wait_unlock(&prompter, serv.store)?) {
			writefmt(client, "error Failed to unlock");
			return;
		};
		prompt::close(&prompter)?;
	};

	const q = query::parse_items(args[1..])?;
	defer query::finish(&q);
	// TODO: Prompt user to fill in incomplete keys
	let entry = match (secstore::add(serv.store, &q)) {
	case secstore::locked =>
		abort(); // TODO
	case let entry: *secstore::entry =>
		yield entry;
	};
	let entry = secstore::add(serv.store, &q)!;
	let buf = bufio::dynamic(io::mode::WRITE);
	fmt::fprint(&buf, "key ")?;
	secstore::write(serv.store, &buf, entry, false)?;
@@ -96,6 +102,17 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {

	let buf = bufio::dynamic(io::mode::WRITE);

	let prompter: (prompt::prompter | void) = void;
	if (serv.store.state != secstore::state::UNLOCKED) {
		const new = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
		prompt::unlock(&new)?;
		if (!prompt::wait_unlock(&new, serv.store)?) {
			writefmt(client, "error Failed to unlock");
			return;
		};
	};

	const iter = secstore::query(serv.store, &q);
	let matches: []*secstore::entry = [];
	for (true) {
@@ -112,8 +129,13 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
	};

	if (len(matches) > 0) {
		const prompter = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
		const prompter = match (prompter) {
		case let p: prompt::prompter =>
			yield p;
		case void =>
			yield prompt::newprompter(serv.conf.prompter[0],
				serv.conf.prompter[1..])?;
		};
		for (let i = 0z; i < len(matches); i += 1) {
			prompt::sendkey(&prompter, serv.store, matches[i])?;
		};
@@ -123,7 +145,13 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
			return;
		};

		secstore::del(serv.store, &q)?;
		secstore::del(serv.store, &q)!;
	} else {
		match (prompter) {
		case let p: prompt::prompter =>
			prompt::close(&p)?;
		case void => yield;
		};
	};

	fmt::fprintln(&buf, "end")?;
@@ -147,6 +175,21 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
		};
	};

	let prompter: (prompt::prompter | void) = void;
	if (serv.store.state == secstore::state::HARD_LOCKED) {
		const new = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
		prompt::unlock(&new)?;
		if (!prompt::wait_unlock(&new, serv.store)?) {
			writefmt(client, "error Failed to unlock");
			return;
		};
		if (!decrypt) {
			prompt::close(&new)?;
		};
		prompter = new;
	};

	const q = query::parse_items(cmd.args)?;
	defer query::finish(&q);

@@ -163,12 +206,26 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
	};

	if (len(matches) > 0 && decrypt) {
		const prompter = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
		const prompter = match (prompter) {
		case let p: prompt::prompter =>
			yield p;
		case void =>
			yield prompt::newprompter(serv.conf.prompter[0],
				serv.conf.prompter[1..])?;
		};
		for (let i = 0z; i < len(matches); i += 1) {
			prompt::sendkey(&prompter, serv.store, matches[i])?;
		};
		if (serv.store.state == secstore::state::SOFT_LOCKED) {
			prompt::unlock(&prompter)?;
		};
		prompt::prompt(&prompter, prompt::mode::DISCLOSE)?;
		if (serv.store.state == secstore::state::SOFT_LOCKED) {
			if (!prompt::wait_unlock(&prompter, serv.store)?) {
				writefmt(client, "error Failed to unlock");
				return;
			};
		};
		if (!prompt::wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
diff --git a/cmd/himitsud/main.ha b/cmd/himitsud/main.ha
index 66d27d8..f89355d 100644
--- a/cmd/himitsud/main.ha
+++ b/cmd/himitsud/main.ha
@@ -1,5 +1,3 @@
use bufio;
use bytes;
use config;
use fmt;
use getopt;
@@ -10,7 +8,6 @@ use os;
use rt;
use secstore;
use unix::signal;
use unix::tty;
use unix;

export fn main() void = {
@@ -29,48 +26,12 @@ export fn main() void = {
		};
	};

	const tty = match (tty::open()) {
	case let file: io::file =>
		yield file;
	case let err: tty::error =>
		fmt::fatal("Error opening tty:", tty::strerror(err));
	};

	const termios = tty::termios_query(tty)!;
	tty::noecho(&termios)!;

	// TODO: There should be more convenient ways of starting himitsud
	// TODO: Better manage secure memory with bufio::scanline/mlock/etc
	fmt::error("Please enter your passphrase to unlock the keyring: ")!;
	const pass = bufio::scanline(tty)!;
	tty::termios_restore(&termios);

	const pass = match (pass) {
	case let buf: []u8 =>
		yield buf;
	case io::EOF =>
		fmt::errorln()!;
		fmt::fatal("Error: no passphrase supplied");
	};
	fmt::errorln()!;

	const store = match (secstore::open()) {
	case let err: secstore::error =>
		bytes::zero(pass);
		fmt::fatal("Error opening secstore:", secstore::strerror(err));
	case let store: secstore::secstore =>
		yield store;
	};
	const err = secstore::unlock(&store, pass);
	bytes::zero(pass);
	free(pass);

	match (err) {
	case let err: secstore::error =>
		fmt::fatal("Error opening secstore:", secstore::strerror(err));
	case void =>
		yield;
	};
	defer secstore::close(&store);

	signal::block(signal::SIGINT, signal::SIGTERM);
diff --git a/prompt/prompter.ha b/prompt/prompter.ha
index cd5889b..cdf8542 100644
--- a/prompt/prompter.ha
+++ b/prompt/prompter.ha
@@ -1,3 +1,4 @@
use bytes;
use bufio;
use errors;
use fmt;
@@ -72,6 +73,11 @@ export fn sendkey(
	fmt::fprintln(prompt.stdin)?;
};

// Sends an unlock request to the prompter.
export fn unlock(prompt: *prompter) (void | error) = {
	fmt::fprintln(prompt.stdin, "unlock")?;
};

// Sends a "prompt" command to the prompter with the given operating mode.
export fn prompt(prompt: *prompter, mode: mode) (void | io::error) = {
	fmt::fprintfln(prompt.stdin, "prompt {}", switch (mode) {
@@ -80,13 +86,45 @@ export fn prompt(prompt: *prompter, mode: mode) (void | io::error) = {
	case mode::DELETE =>
		yield "delete";
	})?;
	io::close(prompt.stdin)?;
	io::close(prompt.stdout)?;
};

// Waits for a password from the prompter and unlocks the store.
export fn wait_unlock(
	prompt: *prompter,
	store: *secstore::secstore,
) (bool | error) = {
	for (true) match (bufio::scanline(prompt.stdout)?) {
	case io::EOF =>
		break;
	case let buf: []u8 =>
		defer {
			bytes::zero(buf);
			free(buf);
		};
		const string = strings::fromutf8_unsafe(buf);

		const (cmd, pass) = strings::cut(string, " ");
		if (cmd != "password") {
			return protoerror;
		};
		match (secstore::unlock(store, strings::toutf8(pass))) {
		case void =>
			fmt::fprintln(prompt.stdin, "password correct")?;
			return true;
		case secstore::badpass =>
			fmt::fprintln(prompt.stdin, "password incorrect")?;
		case let err: secstore::error =>
			abort("Unexpected secstore error");
		};
	};
	return false;
};

// Waits for the prompter to complete and returns true if the user consented to
// the operation, or false otherwise.
export fn wait(prompt: *prompter) (bool | error) = {
	close(prompt)?;

	const res = exec::wait(&prompt.proc)?;
	match (exec::check(&res)) {
	case void =>
@@ -107,3 +145,9 @@ export fn wait(prompt: *prompter) (bool | error) = {
		};
	};
};

// Closes standard input and standard output of a prompter.
export fn close(prompt: *prompter) (void | error) = {
	io::close(prompt.stdin)?;
	io::close(prompt.stdout)?;
};
diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index 2eb2be0..a2884dc 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -62,6 +62,7 @@ export fn create(passphrase: []u8) (secstore | error) = {

	return secstore {
		key = key,
		state = state::UNLOCKED,
		dir = dir,
		index = index,
		entries = [],
@@ -78,6 +79,7 @@ export fn open() (secstore | error) = {
	const index = os::open(path::string(&buf), fs::flags::RDWR)?;
	return secstore {
		key = void,
		state = state::HARD_LOCKED,
		dir = dir,
		index = index,
		entries = [],
@@ -161,6 +163,7 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
	};

	store.key = keystore::newkey(key[..32], "secstore")!;
	store.state = state::UNLOCKED;
	match (load_index(store)) {
	case void => yield;
	case let err: io::error =>
@@ -170,20 +173,22 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = {
	};
};

// Locks the key store, unloading the decryption keys and evicting entries.
// Locks the key store, unloading the decryption keys.
export fn lock(store: *secstore) void = {
	match (store.key) {
	case let key: keystore::key =>
		keystore::destroy(key);
		store.key = void;
	case void =>
		yield;
	};
	free_keys(store);
	store.state = state::SOFT_LOCKED;
};

// Closes the secstore, freeing any associated resources.
export fn close(store: *secstore) void = {
	lock(store);
	free_keys(store);
	io::close(store.index)!;
	free(store.dir);
};
@@ -197,7 +202,7 @@ fn free_keys(store: *secstore) void = {
// Adds an item to the keystore. The provided query must not have any missing
// values.
export fn add(store: *secstore, q: *query::query) (*entry | locked) = {
	if (store.key is void) {
	if (store.state != state::UNLOCKED) {
		return locked;
	};

diff --git a/secstore/serialize.ha b/secstore/serialize.ha
index 2d8dab4..43e6676 100644
--- a/secstore/serialize.ha
+++ b/secstore/serialize.ha
@@ -21,7 +21,7 @@ export fn write(
	ent: *entry,
	private: bool,
) (void | error) = {
	if (store.key is void) {
	if (private && store.state != state::UNLOCKED) {
		return locked;
	};

diff --git a/secstore/types.ha b/secstore/types.ha
index 2039af4..79d42bf 100644
--- a/secstore/types.ha
+++ b/secstore/types.ha
@@ -4,8 +4,20 @@ use fs;
use io;
use uuid;

export type state = enum {
	// The store is fully unlocked
	UNLOCKED,

	// Partially locked - only secret values are encrypted
	SOFT_LOCKED,

	// Fully locked - all key/value entries are encrypted
	HARD_LOCKED,
};

export type secstore = struct {
	key: (keystore::key | void),
	state: state,
	dir: str,
	index: io::handle,
	// TODO: Consider adding hash table or btree or something. Might be fun
-- 
2.36.1
I don't love all of the match statements on the prompter, but we can
address that later. Thanks!

To git@git.sr.ht:~sircmpwn/himitsu
   1bfc63a..d57dd69  master -> master