~sircmpwn/himitsu-devel

himitsu: Implement remembering consent v5 PROPOSED

Willow Barraco: 1
 Implement remembering consent

 9 files changed, 355 insertions(+), 32 deletions(-)
Atm there is no way to preserve a connection over time. To me this is
out of scope for this, as it is implemented here. v3 has been ack by
Drew, at least on the design part.

I think we will add a perconnection option when this become available.

The use-case here imo is periodic scripts that ask for your passwords.
Nothing prevents you from holding a himitsu connection for a while.
hissh-agent does it, for instance.
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/49079/mbox | git am -3
Learn more about email & git

[PATCH himitsu v5] Implement remembering consent Export this patch

This add consentment remembering for some period of time.

Reference: https://todo.sr.ht/~sircmpwn/himitsu/26

This add the prompty commands and replies "remember". The prompter must
reply with the selected remember way after prompt.

Future improvements might also persist consent to disk, rather that
remembering it only for the lifetime of the daemon process. So
"indefinitely" would become "permanently".

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
diff v4: we was starting an useless prompter when remembering if decrypting

cmd/himitsu-store/main.ha   |   3 +-
cmd/himitsud/cmd.ha         | 134 ++++++++++++++++++++++++++++--------
cmd/hiprompt-tty/main.ha    |  78 ++++++++++++++++++++-
config/conf.ha              |  31 +++++++++
docs/himitsu-prompter.5.scd |  17 +++++
docs/himitsu.ini.5.scd      |   8 +++
prompt/prompter.ha          |  98 +++++++++++++++++++++++++-
secstore/secstore.ha        |   2 +
secstore/types.ha           |  16 +++++
9 files changed, 355 insertions(+), 32 deletions(-)

diff --git a/cmd/himitsu-store/main.ha b/cmd/himitsu-store/main.ha
index a4a23ae..c75155a 100644
--- a/cmd/himitsu-store/main.ha
+++ b/cmd/himitsu-store/main.ha
@@ -19,7 +19,8 @@ use net;

// XXX: Distros may want to modify the default config
const conf: str = `[himitsud]
prompter=hiprompt-gtk`;
prompter=hiprompt-gtk
remember=never`;

export fn main() void = {

diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index c6d8dc7..6c6ea5b 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -12,6 +12,7 @@ use secstore;
use shlex;
use strings;
use unix::poll::{event};
use time;

type servererror = !(io::error | fs::error | exec::error | secstore::error);
type cmderror = !(query::error | prompt::error | ...servererror);
@@ -169,9 +170,13 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
			prompt::sendkey(&prompter, serv.store, matches[i])?;
		};
		prompt::prompt(&prompter, prompt::mode::DELETE)?;
		if (!prompt::wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
		match (prompt::wait(&prompter, serv.conf.remembers)?) {
		case let value: bool =>
			if (!value) {
				writefmt(client, "error User declined");
				return;
			};
		case => yield;
		};

		secstore::del(serv.store, &q)!;
@@ -206,15 +211,18 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
	defer if (prompter is prompt::prompter) {
		prompt::close(&(prompter: prompt::prompter))!;
	};
	if (decrypt && serv.store.state != secstore::state::UNLOCKED) {
		prompter = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
	};

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

	const q = query::parse_items(cmd.args)?;
@@ -232,32 +240,64 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
		append(matches, item);
	};

	if (len(matches) > 0 && decrypt) {
		const prompter = match (prompter) {
		case let p: prompt::prompter =>
			// Take the ownership since prompt::wait will also close it
			prompter = void;
			yield p;
		case void =>
			yield prompt::newprompter(serv.conf.prompter[0],
				serv.conf.prompter[1..])?;
		};
	const needconsent = need_consent(serv, matches);
	if (needconsent && (prompter is void)) {
		prompter = prompt::newprompter(serv.conf.prompter[0],
			serv.conf.prompter[1..])?;
	};

	if (len(matches) > 0 && decrypt &&
		(needconsent || serv.store.state != secstore::state::UNLOCKED)) {
		const new = prompter as prompt::prompter;

		for (let i = 0z; i < len(matches); i += 1) {
			prompt::sendkey(&prompter, serv.store, matches[i])?;
			prompt::sendkey(&new, serv.store, matches[i])?;
		};
		if (serv.store.state == secstore::state::SOFT_LOCKED) {
			prompt::unlock(&prompter)?;
			prompt::unlock(&new)?;
		};
		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 (needconsent) {
			if (can_send_remembers(matches)) {
				prompt::send_remembers(&new, serv.conf.remembers)?;
			};
			prompt::prompt(&new, prompt::mode::DISCLOSE)?;
			if (serv.store.state == secstore::state::SOFT_LOCKED) {
				if (!prompt::wait_unlock(&new, serv.store)?) {
					writefmt(client, "error Failed to unlock");
					return;
				};
			};
			defer prompter = void;
			match (prompt::wait(&new, serv.conf.remembers)?) {
			case let value: bool =>
				if (!value) {
					writefmt(client, "error User declined");
					return;
				};
			case let remember: *prompt::remember =>
				const remember = switch (remember.kind) {
				case prompt::remember_kind::NEVER =>
					yield alloc(secstore::remember {
						kind = secstore::remember_kind::NEVER,
						value = void,
					});
				case prompt::remember_kind::INFINITE =>
					yield alloc(secstore::remember {
						kind = secstore::remember_kind::INFINITE,
						value = void,
					});
				case prompt::remember_kind::TIMEOUT =>
					const delta = remember.value as int;
					const now = time::now(time::clock::MONOTONIC);
					yield alloc(secstore::remember {
						kind = secstore::remember_kind::TIMEOUT,
						value = time::add(now, delta * time::SECOND)
					});
				};
				for (let i=0z; i < len(matches); i+=1) {
					append(matches[i].remembers, remember);
				};
			};
		};
		if (!prompt::wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
		};
	};

@@ -280,3 +320,43 @@ fn exec_quit(serv: *server, client: *client, args: []str) (void | cmderror) = {
	};
	serv.terminate = true;
};

fn need_consent(serv: *server, entries: []*secstore::entry) bool = {
	const now = time::now(time::clock::MONOTONIC);
	for (let i=0z; i < len(entries); i+=1) {
		let found = false;
		for (let y=0z; y<len(entries[i].remembers); y+=1) {
			const remember = entries[i].remembers[y];
			switch (remember.kind) {
			case secstore::remember_kind::NEVER =>
				return true;
			case secstore::remember_kind::TIMEOUT =>
				const delta = time::diff(now, remember.value as time::instant);
				if (delta >= 0) {
					found = true;
					break;
				};
			case secstore::remember_kind::INFINITE =>
				return false;
			};
		};
		if (!found) {
			return true;
		};
	};
	return false;
};

fn can_send_remembers(entries: []*secstore::entry) bool = {
	let never_count = 0z;
	for (let i=0z; i < len(entries); i+=1) {
		for (let y=0z; y<len(entries[i].remembers); y+=1) {
			const remember = entries[i].remembers[y];
			if (remember.kind == secstore::remember_kind::NEVER) {
				never_count += 1;
				break;
			};
		};
	};
	return never_count != len(entries);
};
diff --git a/cmd/hiprompt-tty/main.ha b/cmd/hiprompt-tty/main.ha
index 5590763..ddee052 100644
--- a/cmd/hiprompt-tty/main.ha
+++ b/cmd/hiprompt-tty/main.ha
@@ -11,11 +11,13 @@ use strings;
use types;
use unix::signal;
use unix::tty;
use strconv;

type context = struct {
	tty: io::file,
	status: int,
	mode: prompt::mode,
	remembers: [] *prompt::remember,
	keys: []query::query,
};

@@ -45,6 +47,9 @@ export fn main() void = {
		};
		free(ctx.keys);
		io::close(ctx.tty)!;
		for (let i=0z; i < len(ctx.remembers); i+=1) {
			free(ctx.remembers[i]);
		};
	};

	const version = readline(&ctx, &scan, "version");
@@ -61,6 +66,36 @@ export fn main() void = {
			const src = memio::fixed(strings::toutf8(args));
			const key = query::parse(&src)!;
			append(ctx.keys, key);
		case "remember" =>
			const (kind, value) = strings::cut(args, " ");
			const remember = switch (kind) {
			case "never" =>
				yield alloc(prompt::remember {
					kind = prompt::remember_kind::NEVER,
					value = void,
					...
				});
			case "infinite" =>
				yield alloc(prompt::remember {
					kind = prompt::remember_kind::INFINITE,
					value = void,
					...
				});
			case "timeout" =>
				const value = match (strconv::stoi(value)) {
				case let timeout: int => yield timeout;
				case =>
					fmt::fatal("prompt expects timeout integer");
				};
				yield alloc(prompt::remember {
					kind = prompt::remember_kind::TIMEOUT,
					value = value,
					...
				});
			case =>
				fmt::fatal("prompt never, infinite, or timeout, remember kind");
			};
			append(ctx.remembers, remember);
		case "password" =>
			if (args == "incorrect") {
				unlock(&ctx, true);
@@ -139,10 +174,51 @@ fn prompt(ctx: *context) void = {

	if (line == "y" || line == "Y") {
		ctx.status = 0;
		return;
	} else {
		os::exit(1);
	};

	if (len(ctx.remembers) == 0) {
		return;
	};

	fmt::fprint(ctx.tty, "Remember this choice?\n")!;
	for (let i=0z; i < len(ctx.remembers); i+=1) {
		fmt::fprintf(ctx.tty, "{}. ", i+1)!;
		switch (ctx.remembers[i].kind) {
		case prompt::remember_kind::NEVER =>
			fmt::fprint(ctx.tty, "never\n")!;
		case prompt::remember_kind::INFINITE =>
			fmt::fprint(ctx.tty, "indefinitely\n")!;
		case prompt::remember_kind::TIMEOUT =>
			fmt::fprintf(ctx.tty, "for {} seconds.\n", ctx.remembers[i].value as int)!;
		};
	};
	fmt::fprint(ctx.tty, "Choose: ")!;

	const line = match (bufio::scan_line(&scan)) {
	case let line: const str =>
		yield line;
	case =>
		fatal("Error reading user remember");
	};
	const value = match (strconv::stoz(line)) {
	case let value: size => yield value-1;
	case => return;
	};
	if (value < 0 || value >= len(ctx.remembers)) {
		return;
	};

	const remember = ctx.remembers[value];
	switch (remember.kind) {
	case prompt::remember_kind::NEVER =>
		fmt::printfln("remember never")!;
	case prompt::remember_kind::INFINITE =>
		fmt::printfln("remember infinite")!;
	case prompt::remember_kind::TIMEOUT =>
		fmt::printfln("remember timeout {}", remember.value as int)!;
	};
};

fn readline(ctx: *context, scan: *bufio::scanner, want: const str) const str = {
diff --git a/config/conf.ha b/config/conf.ha
index f8c9434..2609ab9 100644
--- a/config/conf.ha
+++ b/config/conf.ha
@@ -7,10 +7,13 @@ use os;
use path;
use shlex;
use strings;
use strconv;
use prompt;

// The himitsu configuration.
export type config = struct {
	prompter: []str,
	remembers: []*prompt::remember,
};

// All possible errors returned by this module.
@@ -74,6 +77,31 @@ fn conf_himitsud(conf: *config, entry: *ini::entry) void = {
		case let items: []str =>
			conf.prompter = items;
		};
	case "remember" =>
		const (kind, value) = strings::cut(entry.2, " ");
		switch (kind) {
		case "infinite" =>
			append(conf.remembers, alloc(prompt::remember {
				kind = prompt::remember_kind::INFINITE,
				value = void,
			}));
		case "timeout" =>
			const timeout = match (strconv::stoi(value)) {
			case let timeout: int => yield timeout;
			case =>
				fmt::fatal("Config error: [himitsud]remember: syntax error");
			};
			append(conf.remembers, alloc(prompt::remember {
				kind = prompt::remember_kind::TIMEOUT,
				value = timeout,
			}));
		case "never" =>
			append(conf.remembers, alloc(prompt::remember {
				kind = prompt::remember_kind::NEVER,
				value = void,
			}));
		case => yield;
		};
	case =>
		yield;
	};
@@ -82,4 +110,7 @@ fn conf_himitsud(conf: *config, entry: *ini::entry) void = {
// Frees resources associated with the Himitsu configuration.
export fn finish(conf: *config) void = {
	strings::freeall(conf.prompter);
	for (let i=0z; i < len(conf.remembers); i+=1) {
		free(conf.remembers[i]);
	};
};
diff --git a/docs/himitsu-prompter.5.scd b/docs/himitsu-prompter.5.scd
index ac04a17..b70663b 100644
--- a/docs/himitsu-prompter.5.scd
+++ b/docs/himitsu-prompter.5.scd
@@ -80,6 +80,15 @@ The following commands are sent from the daemon to the prompter via _stdin_.
	verified. The prompter is encouraged to display some kind of indication
	to the user that this process is underway.

*remember* timeout <value>
*remember* infinite
	Optional commands to notify the prompter of available options for
	remembering user consent. The prompter should reply with the selected
	option, if applicable, after the *prompt* command is received.

	- *timeout*: value exprimed as seconds
	- *infinite*: or until daemon restart

*prompt* disclose|delete
	Sent when the prompter should obtain consent from the user for the
	desired operation, specified by the given parameter. Preceeding this
@@ -147,6 +156,14 @@ The following replies are sent from the prompter to the daemon via _stdout_:
*version* _major_._minor_._patch_
	See *HANDSHAKE* above.

*remember* timeout <value>
*remember* infinite
*remember* never
	Notifies the server that the user indicated a desire to remember their
	consent for this operation. The options selected must match the
	available remember configuration based on the earlier server-initiated
	remember command.

# EXAMPLES

Scenario 1: the keyring is hard locked and the daemon wishes to unlock it.
diff --git a/docs/himitsu.ini.5.scd b/docs/himitsu.ini.5.scd
index 1978cd7..cddaf90 100644
--- a/docs/himitsu.ini.5.scd
+++ b/docs/himitsu.ini.5.scd
@@ -32,6 +32,14 @@ available options are:
	operations. See *himitsu-prompter*(5) for a description of the protocol
	which should be implemented by this executable.

*remember*
	Configures the available configuration options for remembering that the
	user consented to an earlier operation.

	- *timeout* *value*: value expressed as seconds
	- *infinite*: or until daemon restart
	- *never*: disable remembering consent entirely

# SEE ALSO

*himitsu*(7)
diff --git a/prompt/prompter.ha b/prompt/prompter.ha
index 9e3d76a..fed7819 100644
--- a/prompt/prompter.ha
+++ b/prompt/prompter.ha
@@ -9,6 +9,8 @@ use os::exec;
use os;
use secstore;
use strings;
use strconv;
use time;

export type prompter = struct {
	proc: exec::process,
@@ -22,6 +24,17 @@ export type mode = enum {
	DELETE,
};

export type remember_kind = enum {
	NEVER,
	TIMEOUT,
	INFINITE,
};

export type remember = struct {
	kind: remember_kind,
	value: (void | int),
};

// Starts a new prompter session.
export fn newprompter(command: str, args: []str) (prompter | error) = {
	const cmd = exec::cmd(command, args...)?;
@@ -74,6 +87,34 @@ export fn sendkey(
	fmt::fprintln(prompt.stdin)?;
};

// Sends a remember to the prompter.
export fn send_remember(
	prompt: *prompter,
	remember: *remember,
) (void | error) = {
	fmt::fprintf(prompt.stdin, "remember ")?;
	switch (remember.kind) {
	case remember_kind::NEVER =>
		fmt::fprintf(prompt.stdin, "never")?;
	case remember_kind::INFINITE =>
		fmt::fprintf(prompt.stdin, "infinite")?;
	case remember_kind::TIMEOUT =>
		assert(remember.value is int);
		fmt::fprintf(prompt.stdin, "timeout {}", remember.value as int)?;
	};
	fmt::fprintln(prompt.stdin)?;
};

// Sends some remembers to the prompter.
export fn send_remembers(
	prompt: *prompter,
	remembers: []*remember,
) (void | error) = {
	for (let i=0z; i < len(remembers); i+=1) {
		send_remember(prompt, remembers[i])?;
	};
};

// Sends an unlock request to the prompter.
export fn unlock(prompt: *prompter) (void | error) = {
	fmt::fprintln(prompt.stdin, "unlock")?;
@@ -122,13 +163,21 @@ export fn wait_unlock(
};

// 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)?;
// the operation, false otherwise, or a remember if the user agreed to
// remember its consent.
export fn wait(prompt: *prompter, remembers: []*remember) (bool | *remember | error) = {
	io::close(prompt.stdin)?;

	const res = exec::wait(&prompt.proc)?;

	const rem = wait_remember(prompt, remembers)?;
	io::close(prompt.stdout)?;

	match (exec::check(&res)) {
	case void =>
		if (rem is *remember) {
			return rem as *remember;
		};
		return true;
	case let res: !exec::exit_status =>
		match (res) {
@@ -147,6 +196,49 @@ export fn wait(prompt: *prompter) (bool | error) = {
	};
};

// Fetch the user remember option, if given.
export fn wait_remember(
	prompt: *prompter,
	remembers: []*remember,
) (*remember | void | error) = {
	for (true) match (bufio::read_line(prompt.stdout)?) {
	case io::EOF =>
		break;
	case let buf: []u8 =>
		defer {
			bytes::zero(buf);
			free(buf);
		};
		const string = strings::fromutf8_unsafe(buf);

		const (cmd, args) = strings::cut(string, " ");
		if (cmd != "remember") {
			return protoerror;
		};
		const (kind, args) = strings::cut(args, " ");
		for (let i=0z; i < len(remembers); i+=1) {
			switch (remembers[i].kind) {
			case remember_kind::NEVER =>
				if (kind == "never") {
					return remembers[i];
				};
			case remember_kind::INFINITE =>
				if (kind == "infinite") {
					return remembers[i];
				};
			case remember_kind::TIMEOUT =>
				if (kind == "timeout") {
					const value = strconv::stoi(args)!;
					if (value == remembers[i].value as int) {
						return remembers[i];
					};
				};
			};
		};
	};
	return void;
};

// Closes standard input and standard output of a prompter.
export fn close(prompt: *prompter) (void | error) = {
	io::close(prompt.stdin)?;
diff --git a/secstore/secstore.ha b/secstore/secstore.ha
index b67054d..b86f7cb 100644
--- a/secstore/secstore.ha
+++ b/secstore/secstore.ha
@@ -365,6 +365,7 @@ export fn add(store: *secstore, q: *query::query) (*entry | locked | dupentry) =
	};
	append(store.entries, entry {
		pairs = pairs,
		remembers = [],
	});
	let entry = &store.entries[len(store.entries) - 1];
	add_index(store, entry);
@@ -475,6 +476,7 @@ fn load_index(store: *secstore) (void | io::error | errors::invalid) = {
		};
		append(store.entries, entry {
			pairs = pairs,
			remembers = [],
		});
	};
};
diff --git a/secstore/types.ha b/secstore/types.ha
index eae73ed..834b8f3 100644
--- a/secstore/types.ha
+++ b/secstore/types.ha
@@ -3,6 +3,7 @@ use errors;
use fs;
use io;
use uuid;
use time;

export type state = enum {
	// The store is fully unlocked
@@ -27,6 +28,7 @@ export type secstore = struct {

export type entry = struct {
	pairs: []pair,
	remembers: []*remember,
};

export type pair = struct {
@@ -34,6 +36,17 @@ export type pair = struct {
	value: (str | uuid::uuid),
};

export type remember_kind = enum {
	NEVER,
	TIMEOUT,
	INFINITE,
};

export type remember = struct {
	kind: remember_kind,
	value: (void | time::instant),
};

fn entry_finish(ent: *entry) void = {
	for (let i = 0z; i < len(ent.pairs); i += 1) {
		const pair = &ent.pairs[i];
@@ -45,6 +58,9 @@ fn entry_finish(ent: *entry) void = {
			yield;
		};
	};
	for (let i = 0z; i < len(ent.remembers); i += 1) {
		free(ent.remembers[i]);
	};
};

export type badpass = !void;
-- 
2.43.0
Shouldn't consent be remembered on a per-connection basis?

If I authorise one client and pick "remember for an hour", I wouldn't expect
other clients to also be authorised to read those secrets.