~sircmpwn/himitsu-devel

himitsu: Introduce support for remembering consent v1 APPLIED

Armin Preiml: 9
 prompter: parse version as u32
 himitsud: refactor prompter into separate struct
 himitsu::query: add is_sub, is_equal
 document remember options and the persist command
 add remember options as module
 add remember support to prompter
 secstore: export entry_match and add entry_to_query
 himitsud: implement remember support for query
 himitsud: implement persist

 21 files changed, 790 insertions(+), 95 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/53201/mbox | git am -3
Learn more about email & git

[PATCH himitsu 1/9] prompter: parse version as u32 Export this patch

---
 prompt/prompter.ha |  9 ++++-----
 prompt/version.ha  | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 5 deletions(-)
 create mode 100644 prompt/version.ha

diff --git a/prompt/prompter.ha b/prompt/prompter.ha
index 72bb1d6..fbbeace 100644
--- a/prompt/prompter.ha
+++ b/prompt/prompter.ha
@@ -14,6 +14,7 @@ export type prompter = struct {
	proc: exec::process,
	stdin: io::handle,
	stdout: io::handle,
	version: u32,
};

// Prompter operation mode
@@ -35,22 +36,20 @@ export fn newprompter(command: str, args: []str) (prompter | error) = {
	io::close(stdout.1)?;

	fmt::fprintln(stdin.1, "version")?;
	match (bufio::read_line(stdout.0)?) {
	let version = match (bufio::read_line(stdout.0)?) {
	case io::EOF =>
		return protoerror;
	case let buf: []u8 =>
		defer free(buf);
		const string = strings::fromutf8(buf)!;
		if (string != "version 0.0.0") {
			// XXX: Return "unsupported" error instead?
			return protoerror;
		};
		yield parse_version(string)?;
	};

	return prompter {
		proc = proc,
		stdin = stdin.1,
		stdout = stdout.0,
		version = version,
	};
};

diff --git a/prompt/version.ha b/prompt/version.ha
new file mode 100644
index 0000000..df5bf91
--- /dev/null
+++ b/prompt/version.ha
@@ -0,0 +1,32 @@
use fmt;
use strings;
use strconv;

// u32 representation of the version which is `major << 16 | minor << 8 | patch`
type version = enum u32 {
	INIT = 0 << 16 | 0 << 8 | 0,
};

fn parse_version(version: str) (u32 | protoerror) = {
	let tokens = strings::split(version, " ");
	if (len(tokens) != 2 || tokens[0] != "version") {
		return protoerror;
	};

	let nums = strings::split(tokens[1], ".");

	match (version_to_u32(nums)) {
	case let v: u32 =>
		return v;
	case strconv::error =>
		return protoerror;
	};
};

fn version_to_u32(v: []str) (u32 | strconv::error | protoerror) = {
	if (len(v) != 3) return protoerror;

	return strconv::stou8(v[0])?: u32 << 16
		| strconv::stou8(v[1])?: u32 << 8
		| strconv::stou8(v[2])?;
};
-- 
2.45.2

[PATCH himitsu 2/9] himitsud: refactor prompter into separate struct Export this patch

to simplify checking whether prompter has been opened.
---
 cmd/himitsud/cmd.ha      | 102 ++++++++++-----------------------------
 cmd/himitsud/prompter.ha |  65 +++++++++++++++++++++++++
 2 files changed, 90 insertions(+), 77 deletions(-)
 create mode 100644 cmd/himitsud/prompter.ha

diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index 297e1c8..587a8b2 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -81,15 +81,12 @@ 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)?;
	let prompter = new_prompter(serv);
	defer prompter_close(&prompter);

	if (!prompter_unlock(&prompter)?) {
		writefmt(client, "error Failed to unlock");
		return;
	};

	const q = query::parse_items(args[1..])?;
@@ -124,46 +121,24 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
	const q = query::parse_items(cmd.args)?;
	defer query::finish(&q);

	let buf = memio::dynamic();
	let prompter = new_prompter(serv);
	defer prompter_close(&prompter);

	let prompter: (prompt::prompter | void) = void;
	defer if (prompter is prompt::prompter) {
		prompt::close(&(prompter: prompt::prompter))!;
	};
	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;
		};
		prompter = new;
	if (!prompter_unlock(&prompter)?) {
		writefmt(client, "error Failed to unlock");
		return;
	};

	const iter = secstore::query(serv.store, &q, strict);
	let matches: []*secstore::entry = [];
	for (let item => secstore::next(serv.store, &iter)) {
		fmt::fprint(&buf, "key ")?;
		secstore::write(serv.store, &buf, item, false)?;
		io::write(&buf, ['\n'])?;
		append(matches, item);
	};

	if (len(matches) > 0) {
		const prompter = match (prompter) {
		case let p: prompt::prompter =>
			prompter = void;
			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])?;
		};
		prompt::prompt(&prompter, prompt::mode::DELETE)?;
		if (!prompt::wait(&prompter)?) {
		prompter_send_keys(&prompter, matches)?;
		prompter_prompt(&prompter, prompt::mode::DELETE)?;
		if (!prompter_wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
		};
@@ -171,8 +146,7 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
		secstore::del(serv.store, &q, strict)!;
	};

	fmt::fprintln(&buf, "end")?;
	writebuf(client, memio::buffer(&buf));
	writefmt(client, "end");
};

fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
@@ -196,19 +170,13 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
		};
	};

	let prompter: (prompt::prompter | void) = void;
	defer if (prompter is prompt::prompter) {
		prompt::close(&(prompter: prompt::prompter))!;
	};
	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;
		};
		prompter = new;
	let prompter = new_prompter(serv);
	defer prompter_close(&prompter);

	if (serv.store.state == secstore::state::HARD_LOCKED
			&& !prompter_unlock(&prompter)?) {
		writefmt(client, "error Failed to unlock");
		return;
	};

	const q = query::parse_items(cmd.args)?;
@@ -221,29 +189,9 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
	};

	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..])?;
		};
		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)?) {
		prompter_send_keys(&prompter, matches)?;
		prompter_prompt(&prompter, prompt::mode::DISCLOSE)?;
		if (!prompter_wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
		};
diff --git a/cmd/himitsud/prompter.ha b/cmd/himitsud/prompter.ha
new file mode 100644
index 0000000..eeb73c3
--- /dev/null
+++ b/cmd/himitsud/prompter.ha
@@ -0,0 +1,65 @@
use prompt;
use secstore;

type prompter = struct {
	serv: *server,
	prompter: (prompt::prompter | void),
};

fn new_prompter(serv: *server) prompter = prompter {
	serv = serv,
	prompter = void,
};

fn prompter_get(p: *prompter) (prompt::prompter | prompt::error) = {
	match (p.prompter) {
	case let p: prompt::prompter =>
		return p;
	case void =>
		let prompter = prompt::newprompter(p.serv.conf.prompter[0],
			p.serv.conf.prompter[1..])?;
		p.prompter = prompter;
		return prompter;
	};
};

fn prompter_version(p: *prompter) (u32 | prompt::error) =
	prompter_get(p)?.version;

fn prompter_unlock(p: *prompter) (bool | prompt::error) = {
	if (p.serv.store.state == secstore::state::UNLOCKED) {
		return true;
	};

	let prompter = prompter_get(p)?;
	prompt::unlock(&prompter)?;
	return prompt::wait_unlock(&prompter, p.serv.store)?;
};

fn prompter_send_keys(p: *prompter, entries: []*secstore::entry) (void | prompt::error) = {
	let prompter = prompter_get(p)?;

	for (let e .. entries) {
		prompt::sendkey(&prompter, p.serv.store, e)?;
	};
};

fn prompter_prompt(p: *prompter, mode: prompt::mode) (void | prompt::error) = {
	let prompter = prompter_get(p)?;
	prompt::prompt(&prompter, mode)?;
};

fn prompter_wait(p: *prompter) (bool | prompt::error) = {
	let prompter = prompter_get(p)?;
	p.prompter = void;
	return prompt::wait(&prompter)?;
};

fn prompter_close(p: *prompter) void = {
	match (p.prompter) {
	case prompt::prompter =>
		prompt::close(&(p.prompter: prompt::prompter))!;
	case void => void;
	};
	p.prompter = void;
};
-- 
2.45.2

[PATCH himitsu 3/9] himitsu::query: add is_sub, is_equal Export this patch

---
 himitsu/query/parse.ha | 53 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 53 insertions(+)

diff --git a/himitsu/query/parse.ha b/himitsu/query/parse.ha
index 3de81b8..5d1f359 100644
--- a/himitsu/query/parse.ha
+++ b/himitsu/query/parse.ha
@@ -147,6 +147,59 @@ export fn dup_pub(q: *query) query = {
	return query;
};

// Checks whether 'sub' is a sub-query of 'q'. It returns true if all
// pairs of 'sub' are part of 'q'.
export fn is_sub(sub: *query, q: *query) bool = {
	for :sub (let s &.. sub.items) {
		for (let i &..q.items) {
			if (i.key == s.key && i.optional == s.optional
					&& i.private == s.private
					&& (i.private || i.value == s.value)) {
				continue: sub;
			};
		};
		return false;
	};
	return true;
};

@test fn is_sub() void = {
	let q = parse_str("a=b x=y c? d!")!;
	defer finish(&q);

	assert(is_sub(&q, &q));

	let s = parse_str("a=b d!")!;
	defer finish(&s);
	assert(is_sub(&s, &q));

	let s = parse_str("a=b i! d!")!;
	defer finish(&s);
	assert(!is_sub(&s, &q));
};

// Checks whether 'a' is equal to 'b'.
export fn is_equal(a: *query, b: *query) bool =
	len(a.items) == len(b.items) && is_sub(a, b);

@test fn is_equal() void = {
	let q = parse_str("a=b x=y c? d!")!;
	defer finish(&q);
	assert(is_equal(&q, &q));

	let s = parse_str("a=b x=y c? d!")!;
	defer finish(&s);
	assert(is_equal(&s, &q));

	let s = parse_str("a=b x=z c? d!")!;
	defer finish(&s);
	assert(!is_equal(&s, &q));

	let s = parse_str("a=b c? d!")!;
	defer finish(&s);
	assert(!is_equal(&s, &q));
};

// Frees resources associated with this query.
export fn finish(q: *query) void = {
	for (let item .. q.items) {
-- 
2.45.2

[PATCH himitsu 4/9] document remember options and the persist command Export this patch

I've continued from Willow Barraco's initail work.

Co-Author: Willow Barraco <contact@willowbarraco.fr>
---
 docs/himitsu-ipc.5.scd      | 30 ++++++++++++++++++---
 docs/himitsu-prompter.5.scd | 52 ++++++++++++++++++++++++++++++++++---
 2 files changed, 74 insertions(+), 8 deletions(-)

diff --git a/docs/himitsu-ipc.5.scd b/docs/himitsu-ipc.5.scd
index ea1caa4..251ae08 100644
--- a/docs/himitsu-ipc.5.scd
+++ b/docs/himitsu-ipc.5.scd
@@ -41,12 +41,31 @@ The following commands are recognized by the server:
	a *key* reply for each deleted key (without secrets), followed by an
	*end* reply. The option -s will enable the strict query mode.

*query* [-ds] _query_...
*persist* [-rs] _query_
	Persists a query after user consent for current connection. Private
	values that match the given query will be decrypted without user
	consent, if the store is unlocked. The server will answer with a
	*persist* reply.

	- *-r <remember options>*: as a comma separated list that are send to
	  the prompter. Allowed values are the strings _session_, _skip_,
	  _refuse_, or a timeout in section as an integer. Eg.: "-r
	  session,3600,300,refuse". The first value is the default value and
	  should be preselected by the prompter. See the *remember* command in
	  *himitsu-prompter(5)* for a description of the options.
	- *-s*: will enable the strict query mode.

*query* [-drs] _query_...
	Queries the key store for keys matching the provided _query_. Matching
	keys are returned via a series of *key* replies, terminated with an
	*end* reply to signal the end of the list. If the -d option is provided,
	the private values will be decrypted after requesting user consent. The
	option -s will enable the strict query mode.
	*end* reply to signal the end of the list.

	- *-d*: the private values will be decrypted after requesting user
	  consent.
	- *-r <remember options>*: as a comma separated list similar to the
	  same option of the *persist* command. If the user agrees, the entries
	  matched by given query will be remembered for the current connection.
	- *-s*: will enable the strict query mode.

*quit*
	Requests that the daemon terminate itself. This command is only
@@ -63,6 +82,9 @@ The following replies are sent to the client:
*end*
	Signals the end of a list of *key* replies.

*persist* session|timeout <value>|skip|refuse
	Returns the selected remember option on *persist*.

*error* _message_...
	Indicates that a requested operation resulted in an error. The _message_
	is both human-friendly and consistent, such that it can be displayed to
diff --git a/docs/himitsu-prompter.5.scd b/docs/himitsu-prompter.5.scd
index ac04a17..98ad1ce 100644
--- a/docs/himitsu-prompter.5.scd
+++ b/docs/himitsu-prompter.5.scd
@@ -49,7 +49,7 @@ discretion if this protocol version is suitable, and if so, proceed to issue
additional commands; if not, it will close stdin and the prompter should exit
with status code 1.

The current protocol number is 0.0.0.
The current protocol number is 0.0.1.

## EXIT STATUS

@@ -73,6 +73,11 @@ The following commands are sent from the daemon to the prompter via _stdin_.
	Provides a key to the prompter which is related to this transaction, in
	the format described by *KEY FORMAT* in *himitsu*(7).

*query* _query_
	Provides a query to the prompter which is related to this transaction,
	in the format described by *QUERY SYNTAX* in *himitsu*(7). A query
	command preceeds the *prompt persist* command.

*password* correct|incorrect
	Provides feedback in response to the *password* reply sent from the
	prompter to the daemon. Note that following the *password* reply from
@@ -80,17 +85,37 @@ 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.

*prompt* disclose|delete
*remember* session|timeout <value>|skip|refuse
	Optional commands to notify the prompter of available options for
	remembering user consent. The prompter should preselect the first
	option received and reply with the selected one, after the *prompt*
	command. The user consent is remembered for the client requesting it.

	- *session*: Until the daemon stops.
	- *timeout <value>*: Until value as seconds have passed or until the
	  session ends, if latter happens earlier.
	- *skip*: Do not remember for now, but may be asked next time.
	- *refuse*: Do not remember and do not ask again for current session.
	  In this case, no remember commands will be send during the remaining
	  session for the query or keys in question.

*prompt* disclose|delete|persist
	Sent when the prompter should obtain consent from the user for the
	desired operation, specified by the given parameter. Preceeding this
	command will be one or more *key* commands describing the keys
	implicated in this operation.
	implicated in this operation, in the case of *disclose* or *delete*.
	On *persist* a *query* command preceeds.

	- *disclose*: the specified keys will be disclosed to the client,
	  including secret values.
	- *delete*: the specified keys will be removed from the key store
	- *persist*: the user is asked if the specified query should be
	  remembered. If so, future disclosures of secrets that are matched by
	  the query will be allowed without user consent depending on the
	  selected remember option defined at the *remember* command.

	Following this command, the daemon will close _stdin_ and will wait for
	Following this command, the daemon will wait for a answer to
	given remember options and/or close _stdin_ and will wait for
	the prompter to exit with a meaningful status code as described by *EXIT
	STATUS* above.

@@ -144,6 +169,10 @@ The following replies are sent from the prompter to the daemon via _stdout_:
	to complete. The prompter implementation is encouraged to provide
	feedback to the user to indicate that the operation is underway.

*remember* session|timeout <value>|skip|refuse
	Notifies the server that the user indicated a desire to remember their
	consent for this operation.

*version* _major_._minor_._patch_
	See *HANDSHAKE* above.

@@ -194,6 +223,21 @@ user's secret keys.
(prompter exits with status code 0)
```

Scenario 4: the keyring is unlocked and a client wants to persist a query.

```
=> version
<= version 0.0.1
=> query proto=web
=> remember session
=> remember timeout 300
=> remember refuse
=> prompt persist
<= remember session
(daemon closes stdin)
(prompter exits with status code0)
```

# SEE ALSO

*himitsu*(7)
-- 
2.45.2

[PATCH himitsu 5/9] add remember options as module Export this patch

My inital approach was an enum with session, skip and refuse + a
timestamp type. It was tedious to handle since you had to have a match +
switch. I didn't find proper namings of those types to include them in
himitsu::client that's why I've decided to create an extra module.
---
 Makefile                  |  2 ++
 himitsu/remember/types.ha | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 34 insertions(+)
 create mode 100644 himitsu/remember/types.ha

diff --git a/Makefile b/Makefile
index b972d4f..85d8753 100644
--- a/Makefile
+++ b/Makefile
@@ -55,6 +55,7 @@ install:
	mkdir -p $(DESTDIR)$(PREFIX)/bin
	mkdir -p $(DESTDIR)$(THIRDPARTYDIR)/himitsu/client
	mkdir -p $(DESTDIR)$(THIRDPARTYDIR)/himitsu/query
	mkdir -p $(DESTDIR)$(THIRDPARTYDIR)/himitsu/remember
	mkdir -p $(DESTDIR)$(MANDIR)/man1
	mkdir -p $(DESTDIR)$(MANDIR)/man5
	mkdir -p $(DESTDIR)$(MANDIR)/man7
@@ -62,6 +63,7 @@ install:
	install -m644 himitsu/README $(DESTDIR)$(THIRDPARTYDIR)/himitsu
	install -m644 himitsu/client/* $(DESTDIR)$(THIRDPARTYDIR)/himitsu/client
	install -m644 himitsu/query/* $(DESTDIR)$(THIRDPARTYDIR)/himitsu/query
	install -m644 himitsu/remember/* $(DESTDIR)$(THIRDPARTYDIR)/himitsu/remember
	install -m644 himitsud.1 $(DESTDIR)$(MANDIR)/man1/himitsud.1
	install -m644 himitsu-store.1 $(DESTDIR)$(MANDIR)/man1/himitsu-store.1
	install -m644 hiq.1 $(DESTDIR)$(MANDIR)/man1/hiq.1
diff --git a/himitsu/remember/types.ha b/himitsu/remember/types.ha
new file mode 100644
index 0000000..610c65f
--- /dev/null
+++ b/himitsu/remember/types.ha
@@ -0,0 +1,32 @@
use fmt;

// Remembers consent until daemon stops
export type session = void;

// Skips remembering consent, but will ask the next time
export type skip = void;

// Refuse to remember consent and don't ask again
export type refuse = void;

// Remember consent for given timeout in seconds.
export type timeout = i64;

// An option for rembering consent
export type option = (session | timeout | skip | refuse);

// String representation of a remember option
export fn stroption(o: option) str = {
	static let buf: [32] u8 = [0...];
	match (o) {
	case session =>
		return "session";
	case skip =>
		return "skip";
	case refuse =>
		return "refuse";
	case let t: timeout =>
		return fmt::bsprintf(buf, "timeout {}", t: i64);
	};
};

-- 
2.45.2

[PATCH himitsu 6/9] add remember support to prompter Export this patch

---
 cmd/himitsud/prompter.ha | 16 ++++++++++
 prompt/prompter.ha       | 64 ++++++++++++++++++++++++++++++++++++++++
 prompt/version.ha        |  3 +-
 3 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/cmd/himitsud/prompter.ha b/cmd/himitsud/prompter.ha
index eeb73c3..4177746 100644
--- a/cmd/himitsud/prompter.ha
+++ b/cmd/himitsud/prompter.ha
@@ -1,3 +1,4 @@
use himitsu::remember;
use prompt;
use secstore;

@@ -44,6 +45,21 @@ fn prompter_send_keys(p: *prompter, entries: []*secstore::entry) (void | prompt:
	};
};

fn prompter_send_remember(
	p: *prompter,
	rememberopts: []remember::option,
) (void | prompt::error) = {
	let prompter = prompter_get(p)?;
	prompt::send_remember(&prompter, rememberopts)?;
};

fn prompter_wait_remember(
	p: *prompter,
) (remember::option | prompt::error) = {
	let prompter = prompter_get(p)?;
	return prompt::wait_remember(&prompter)?;
};

fn prompter_prompt(p: *prompter, mode: prompt::mode) (void | prompt::error) = {
	let prompter = prompter_get(p)?;
	prompt::prompt(&prompter, mode)?;
diff --git a/prompt/prompter.ha b/prompt/prompter.ha
index fbbeace..55abb87 100644
--- a/prompt/prompter.ha
+++ b/prompt/prompter.ha
@@ -3,12 +3,14 @@ use bufio;
use errors;
use fmt;
use fs;
use himitsu::remember;
use io;
use memio;
use os::exec;
use os;
use secstore;
use strings;
use strconv;

export type prompter = struct {
	proc: exec::process,
@@ -73,6 +75,68 @@ export fn sendkey(
	fmt::fprintln(prompt.stdin)?;
};

// Sends recommended remember options, if the prompter supports it.
export fn send_remember(
	prompt: *prompter,
	options: []remember::option,
) (void | error) = {
	if (prompt.version < version::REMEMBER) {
		return;
	};

	for (let o .. options) {
		match (o) {
		case let t: remember::timeout =>
			fmt::fprintln(prompt.stdin, "remember timeout", t: i64)?;
		case let r: remember::option =>
			fmt::fprintln(prompt.stdin, "remember",
				remember::stroption(r: remember::option))?;
		};
	};
};

// Awaits a remember response. Returns void if none received.
export fn wait_remember(prompt: *prompter) (remember::option | error) = {
	if (prompt.version < version::REMEMBER) {
		return remember::refuse;
	};

	let buf = match (bufio::read_line(prompt.stdout)?) {
	case let b: []u8 =>
		yield b;
	case io::EOF =>
		return remember::skip;
	};

	defer free(buf);

	const string = strings::fromutf8_unsafe(buf);

	const (cmd, args) = strings::cut(string, " ");
	if (cmd != "remember") {
		return protoerror;
	};

	const (method, args) = strings::cut(args, " ");
	switch (method) {
	case "session" =>
		return remember::session;
	case "skip" =>
		return remember::skip;
	case "refuse" =>
		return remember::refuse;
	case "timeout" =>
		match (strconv::stou32(args)) {
		case let t: u32 =>
			return t: remember::timeout;
		case strconv::error =>
			return protoerror;
		};
	case =>
		return protoerror;
	};
};

// Sends an unlock request to the prompter.
export fn unlock(prompt: *prompter) (void | error) = {
	fmt::fprintln(prompt.stdin, "unlock")?;
diff --git a/prompt/version.ha b/prompt/version.ha
index df5bf91..56cdcf6 100644
--- a/prompt/version.ha
+++ b/prompt/version.ha
@@ -3,8 +3,9 @@ use strings;
use strconv;

// u32 representation of the version which is `major << 16 | minor << 8 | patch`
type version = enum u32 {
export type version = enum u32 {
	INIT = 0 << 16 | 0 << 8 | 0,
	REMEMBER = 0 << 16 | 0 << 8 | 1,
};

fn parse_version(version: str) (u32 | protoerror) = {
-- 
2.45.2

[PATCH himitsu 7/9] secstore: export entry_match and add entry_to_query Export this patch

both are required to check and remember entries
---
 secstore/query.ha | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/secstore/query.ha b/secstore/query.ha
index 089571f..18ec4f7 100644
--- a/secstore/query.ha
+++ b/secstore/query.ha
@@ -1,4 +1,5 @@
use himitsu::query;
use strings;
use uuid;

export type iterator = struct {
@@ -36,7 +37,8 @@ export fn next(store: *secstore, iter: *iterator) (*entry | done) = {
	return done;
};

fn entry_match(ent: *entry, query: *query::query, strict: bool) bool = {
// Returns whether 'query' matches 'ent'.
export fn entry_match(ent: *entry, query: *query::query, strict: bool) bool = {
	let nmatched = 0z;
	for (let q &.. query.items) {
		const p = match (findpair(ent, q.key)) {
@@ -75,3 +77,27 @@ fn findpair(ent: *entry, key: str) (*pair | void) = {
		};
	};
};

// Converts an entry to it's matching query. The caller must finish the query
// with [[himitsu::query::finish]].
export fn entry_to_query(ent: *entry) query::query = {
	let q = query::query {
		...
	};
	for (let p &.. ent.pairs) {
		let (value, private) = match (p.value) {
		case let v: str =>
			yield (strings::dup(v), false);
		case uuid::uuid =>
			yield ("", true);
		};

		append(q.items, query::pair {
			key = strings::dup(p.key),
			value = value,
			private = private,
			optional = false,
		});
	};
	return q;
};
-- 
2.45.2

[PATCH himitsu 8/9] himitsud: implement remember support for query Export this patch

Remember options will be passed by the client using the -r option. Those
options are suggestions given by the prompter. The prompter can still
chose a option that has not been recommended.
---
 cmd/himitsud/client.ha   |  15 +++++
 cmd/himitsud/cmd.ha      |  50 ++++++++++++++-
 cmd/himitsud/remember.ha | 134 +++++++++++++++++++++++++++++++++++++++
 himitsu/client/client.ha |  61 ++++++++++++++++++
 4 files changed, 257 insertions(+), 3 deletions(-)
 create mode 100644 cmd/himitsud/remember.ha

diff --git a/cmd/himitsud/client.ha b/cmd/himitsud/client.ha
index 3303ebf..74324a7 100644
--- a/cmd/himitsud/client.ha
+++ b/cmd/himitsud/client.ha
@@ -1,4 +1,6 @@
use fmt;
use himitsu::query;
use himitsu::remember;
use io;
use strings;
use unix::poll;
@@ -10,6 +12,13 @@ type state = enum {
	WRITE_ERROR,
};

type remember = struct {
	query: query::query,
	kind: remember::option,
	strict: bool,
	timestamp: i64,
};

type client = struct {
	server: *server,
	sock: io::file,
@@ -17,6 +26,7 @@ type client = struct {
	pollfd: *poll::pollfd,
	rbuf: []u8,
	wbuf: []u8,
	remembers: []remember,
};

// Immediately disconnects a client, without sending them an error message.
@@ -25,6 +35,11 @@ fn disconnect(c: *client) void = {
	free(c.rbuf);
	free(c.wbuf);

	for (let r &.. c.remembers) {
		query::finish(&r.query);
	};
	free(c.remembers);

	let serv = c.server;
	let i = (c: uintptr - serv.clients: *[*]client: uintptr): size / size(client);
	delete(serv.clients[i]);
diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index 587a8b2..9a1b369 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -4,13 +4,16 @@ use fmt;
use fs;
use getopt;
use himitsu::query;
use himitsu::remember;
use io;
use log;
use os::exec;
use prompt;
use secstore;
use shlex;
use strconv;
use strings;
use time;
use unix::poll::{event};

type servererror = !(io::error | fs::error | exec::error | secstore::error);
@@ -153,6 +156,7 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
	const cmd = getopt::parse(args,
		"query the key store",
		('d', "decrypt private keys"),
		('r', "options", "Suggested remember options"),
		('s', "strict match"),
		"query..."
	);
@@ -160,10 +164,16 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {

	let decrypt = false;
	let strict = false;
	let remopts: []remember::option = [];
	defer free(remopts);

	for (let i = 0z; i < len(cmd.opts); i += 1) {
		switch (cmd.opts[i].0) {
		const opt = cmd.opts[i];
		switch (opt.0) {
		case 'd' =>
			decrypt = true;
		case 'r' =>
			remopts = parse_remopts(opt.1)?;
		case 's' =>
			strict = true;
		case => abort();
@@ -184,13 +194,47 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {

	const iter = secstore::query(serv.store, &q, strict);
	let matches: []*secstore::entry = [];
	defer free(matches);

	for (let item => secstore::next(serv.store, &iter)) {
		append(matches, item);
	};

	if (len(matches) > 0 && decrypt) {
		prompter_send_keys(&prompter, matches)?;
	let unrem: []*secstore::entry = [];
	let entry_refused = false;
	defer free(unrem);

	if (decrypt && !is_remembered(client, &q)) {
		unrem = filter_unremembered(client, matches, &entry_refused);
	};

	if (len(matches) > 0 && decrypt && len(unrem) > 0) {
		if (serv.store.state == secstore::state::SOFT_LOCKED
				&& !prompter_unlock(&prompter)?) {
			writefmt(client, "error Failed to unlock");
			return;
		};

		prompter_send_keys(&prompter, unrem)?;

		if (prompter_version(&prompter)? >= prompt::version::REMEMBER
				&& !entry_refused) {
			prompter_send_remember(&prompter, remopts)?;
		};

		prompter_prompt(&prompter, prompt::mode::DISCLOSE)?;

		if (prompter_version(&prompter)? >= prompt::version::REMEMBER) {
			match (prompter_wait_remember(&prompter)?) {
			case remember::skip => void;
			case let r: remember::option =>
				for (let m .. matches) {
					let query = secstore::entry_to_query(m);
					add_remember(client, query, r, true);
				};
			};
		};

		if (!prompter_wait(&prompter)?) {
			writefmt(client, "error User declined");
			return;
diff --git a/cmd/himitsud/remember.ha b/cmd/himitsud/remember.ha
new file mode 100644
index 0000000..c99f816
--- /dev/null
+++ b/cmd/himitsud/remember.ha
@@ -0,0 +1,134 @@
use himitsu::query;
use himitsu::remember;
use prompt;
use secstore;
use strconv;
use strings;
use time;

fn parse_remopts(opts: str) ([]remember::option | prompt::protoerror) = {
	let result: []remember::option = [];
	for (let o .. strings::split(opts, ",")) {
		switch (o) {
		case "session" =>
			append(result, remember::session);
		case "skip" =>
			append(result, remember::skip);
		case "refuse" =>
			append(result, remember::refuse);
		case =>
			match (strconv::stou32(o)) {
			case let t: u32 =>
				append(result, t: remember::timeout);
			case strconv::error =>
				free(result);
				return prompt::protoerror;
			};
		};
	};
	return result;
};

fn filter_unremembered(
	client: *client,
	matches: []*secstore::entry,
	entry_refused: *bool,
) []*secstore::entry = {
	let unrem: []*secstore::entry = [];
	for (let m .. matches) {
		let r = find_remember(client, m);
		if (r is remember::refuse) {
			*entry_refused = true;
		};

		match (r) {
		case (remember::session | remember::timeout) => void;
		case (remember::skip | remember::refuse) =>
			append(unrem, m);
		};
	};
	return unrem;
};

fn add_remember(
	client: *client,
	query: query::query,
	kind: remember::option,
	strict: bool
) void = {
	if (kind is remember::skip) {
		return;
	};

	let now = time::now(time::clock::MONOTONIC);
	append(client.remembers, remember {
		query = query,
		strict = strict,
		kind = kind,
		timestamp = time::unix(now),
	});
};

// Finds remember for given query or secstore entry. A remembered query matches
// given query, if it the remembered one is a sub query. If there are multiple
// query matches the one with the highes precedence is chosen (refuse > session
// > timeout).
//
// If no remember was found, remember::skip is returned.
fn find_remember(
	c: *client,
	q: (*query::query | *secstore::entry)
) remember::option = {
	let option: remember::option = remember::skip;
	for (let i = 0z; i < len(c.remembers); i += 1) {
		let r = &c.remembers[i];

		match (q) {
		case let q: *query::query =>
			if ((r.strict && !query::is_equal(&r.query, q))
					|| (!r.strict && !query::is_sub(&r.query, q))) {
				continue;
			};
		case let e: *secstore::entry =>
			if (!secstore::entry_match(e, &r.query, true)) {
				continue;
			};
		};

		match (r.kind) {
		case let t: remember::timeout =>
			if (option is remember::session) {
				continue;
			};
			let now = time::unix(time::now(time::clock::MONOTONIC));
			assert(now > 0);
			let passed = now - r.timestamp;
			if (passed >= t) {
				delete(c.remembers[i]);
				i -= 1;
				continue;
			};
			option = t - passed;
		case remember::session =>
			option = remember::session;
		case remember::refuse =>
			return remember::refuse;
		case remember::skip =>
			abort("skip must not be stored");
		};

		if (q is *secstore::entry) {
			return option;
		};
	};
	return option;
};

fn is_remembered(c: *client, q: (*query::query | *secstore::entry)) bool = {
	match (find_remember(c, q)) {
	case (remember::timeout | remember::session) =>
		return true;
	case (remember::skip | remember::refuse) =>
		return false;
	};
};
diff --git a/himitsu/client/client.ha b/himitsu/client/client.ha
index 3aa4a9d..a77611b 100644
--- a/himitsu/client/client.ha
+++ b/himitsu/client/client.ha
@@ -3,11 +3,13 @@ use dirs;
use errors;
use fmt;
use himitsu::query;
use himitsu::remember;
use io;
use net::unix;
use net;
use path;
use shlex;
use strconv;
use strings;
use memio;
use encoding::utf8;
@@ -29,6 +31,10 @@ export type flags = enum uint {
	STRICT = 1 << 1,
};

export type options = struct {
	remember: []remember::option,
};

// All possible errors which may be returned by this module.
export type error = !(io::error | net::error | hierror);

@@ -57,6 +63,27 @@ export fn connect() (net::socket | error) = {
	return unix::connect(sockpath)?;
};

export fn parse_remembercmd(cmd: str) (remember::option | errors::invalid) = {
	const (method, arg) = strings::cut(cmd, " ");
	switch (method) {
	case "session" =>
		return remember::session;
	case "skip" =>
		return remember::skip;
	case "refuse" =>
		return remember::refuse;
	case "timeout" =>
		match (strconv::stou32(arg)) {
		case let t: u32 =>
			return t: remember::timeout;
		case strconv::error =>
			return errors::invalid;
		};
	case =>
		return errors::invalid;
	};
};

export type keyiter = struct {
	conn: net::socket,
	buf: memio::stream,
@@ -70,6 +97,7 @@ export fn query(
	op: operation,
	q: *query::query,
	flags: flags,
	opts: (void | options) = void,
) (keyiter | error) = {
	const buf = memio::dynamic();
	defer io::close(&buf)!;
@@ -93,6 +121,8 @@ export fn query(
		fmt::fprint(&buf, "-s ")!;
	};

	write_remopts(&buf, opts)?;

	query::unparse(&buf, q)!;

	io::writeall(conn, memio::buffer(&buf))?;
@@ -103,6 +133,37 @@ export fn query(
	};
};

fn write_remopts(
	buf: io::handle,
	opts: (options | void)
) (void | hierror) = {
	let opts: []remember::option = match (opts) {
	case void =>
		return;
	case let o: options =>
		yield o.remember;
	};

	if (len(opts) == 0) {
		return;
	};

	fmt::fprint(buf, "-r ")!;
	let first = true;
	for (let r .. opts) {
		if (!first) fmt::fprint(buf, ",")! else first = false;

		match (r) {
		case let t: remember::timeout =>
			fmt::fprintf(buf, "{}", t)!;
		case let p: remember::option =>
			fmt::fprint(buf, remember::stroption(p))!;
		};
	};

	fmt::fprint(buf, " ")!;
};

// Returns the next key from a key iterator, or void if no further keys are
// provided.
export fn next(iter: *keyiter) (query::query | void | error) = {
-- 
2.45.2

[PATCH himitsu 9/9] himitsud: implement persist Export this patch

---
 cmd/himitsud/cmd.ha      | 66 ++++++++++++++++++++++++++++++++++++++++
 cmd/himitsud/prompter.ha |  6 ++++
 himitsu/client/client.ha | 52 +++++++++++++++++++++++++++++++
 prompt/prompter.ha       | 13 ++++++++
 4 files changed, 137 insertions(+)

diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index 9a1b369..5148cd3 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -58,6 +58,8 @@ fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = {
		yield &exec_del;
	case "query" =>
		yield &exec_query;
	case "persist" =>
		yield &exec_persist;
	case "quit" =>
		yield &exec_quit;
	case =>
@@ -253,6 +255,7 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
	writebuf(client, memio::buffer(&buf));
};


fn exec_quit(serv: *server, client: *client, args: []str) (void | cmderror) = {
	if (!serv.daemonized) {
		writefmt(client, "error Server is not damonized, use a service manager");
@@ -260,3 +263,66 @@ fn exec_quit(serv: *server, client: *client, args: []str) (void | cmderror) = {
	};
	serv.terminate = true;
};

fn exec_persist(serv: *server, client: *client, args: []str) (void | cmderror) = {
	const cmd = getopt::parse(args,
		"persist a query for future disclosures",
		('r', "options", "Suggested remember options"),
		('s', "Save as strict query"),
		"query..."
	);
	defer getopt::finish(&cmd);

	let remopts: []remember::option = [];
	defer free(remopts);

	let strict = false;
	for (let i = 0z; i < len(cmd.opts); i += 1) {
		const opt = cmd.opts[i];
		switch (opt.0) {
		case 'r' =>
			remopts = parse_remopts(opt.1)?;
		case 's' =>
			strict = true;
		case => abort();
		};
	};

	let prompter = new_prompter(serv);
	defer prompter_close(&prompter);

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

	match (find_remember(client, &q)) {
	case remember::skip => void;
	case let o: remember::option =>
		writefmt(client, "persist {}", remember::stroption(o));
		return;
	};

	if (prompter_version(&prompter)? < prompt::version::REMEMBER) {
		writefmt(client, "error Not supported by prompter");
		return;
	};

	prompter_send_query(&prompter, &q)?;
	prompter_send_remember(&prompter, remopts)?;
	prompter_prompt(&prompter, prompt::mode::PERSIST)?;

	const result = prompter_wait_remember(&prompter)?;

	if (!prompter_wait(&prompter)?) {
		writefmt(client, "persist skip");
		return;
	};

	match (result) {
	case remember::skip =>
		writefmt(client, "persist skip");
	case let r: remember::option =>
		add_remember(client, query::dup_pub(&q), r, strict);
		writefmt(client, "persist {}", remember::stroption(r));
	};
};

diff --git a/cmd/himitsud/prompter.ha b/cmd/himitsud/prompter.ha
index 4177746..2f9a885 100644
--- a/cmd/himitsud/prompter.ha
+++ b/cmd/himitsud/prompter.ha
@@ -1,3 +1,4 @@
use himitsu::query;
use himitsu::remember;
use prompt;
use secstore;
@@ -45,6 +46,11 @@ fn prompter_send_keys(p: *prompter, entries: []*secstore::entry) (void | prompt:
	};
};

fn prompter_send_query(p: *prompter, q: *query::query) (void | prompt::error) = {
	let prompter = prompter_get(p)?;
	prompt::send_query(&prompter, q)?;
};

fn prompter_send_remember(
	p: *prompter,
	rememberopts: []remember::option,
diff --git a/himitsu/client/client.ha b/himitsu/client/client.ha
index a77611b..8c35a13 100644
--- a/himitsu/client/client.ha
+++ b/himitsu/client/client.ha
@@ -206,3 +206,55 @@ export fn next(iter: *keyiter) (query::query | void | error) = {
	};
	abort("himitsu returned unexpected response");
};

export fn persist(
	conn: net::socket,
	q: *query::query,
	flags: flags = 0,
	opts: (void | options) = void,
) (remember::option | error) = {
	const buf = memio::dynamic();
	defer io::close(&buf)!;

	fmt::fprint(&buf, "persist ")!;

	if (flags & flags::STRICT != 0) {
		fmt::fprint(&buf, "-s ")!;
	};

	write_remopts(&buf, opts)?;
	query::unparse(&buf, q)!;
	io::writeall(conn, memio::buffer(&buf))?;

	let buf = match (bufio::read_line(conn)?) {
	case let buf: []u8 =>
		yield buf;
	case io::EOF =>
		return "Internal error": hierror;
	};
	defer free(buf);

	const line = match (strings::fromutf8(buf)) {
	case let s: str =>
		yield s;
	case utf8::invalid =>
		return errors::invalid: io::error;
	};

	if (strings::hasprefix(line, "error ")) {
		// TODO: leaks
		return strings::dup(strings::sub(line, 6)): hierror: error;
	};
	if (strings::hasprefix(line, "persist ")) {
		const (_, args) = strings::cut(line, " ");
		match (parse_remembercmd(args)) {
		case let r: remember::option =>
			return r;
		case errors::invalid =>
			return "Internal error": hierror;
		};
	};

	return "Internal error": hierror;
};

diff --git a/prompt/prompter.ha b/prompt/prompter.ha
index 55abb87..ccb252f 100644
--- a/prompt/prompter.ha
+++ b/prompt/prompter.ha
@@ -3,6 +3,7 @@ use bufio;
use errors;
use fmt;
use fs;
use himitsu::query;
use himitsu::remember;
use io;
use memio;
@@ -23,6 +24,7 @@ export type prompter = struct {
export type mode = enum {
	DISCLOSE,
	DELETE,
	PERSIST,
};

// Starts a new prompter session.
@@ -137,6 +139,15 @@ export fn wait_remember(prompt: *prompter) (remember::option | error) = {
	};
};

// Sends a query to the prompter
export fn send_query(
	prompt: *prompter,
	query: *query::query,
) (void | error) = {
	fmt::fprintf(prompt.stdin, "query ")?;
	query::unparse(prompt.stdin, query)?;
};

// Sends an unlock request to the prompter.
export fn unlock(prompt: *prompter) (void | error) = {
	fmt::fprintln(prompt.stdin, "unlock")?;
@@ -149,6 +160,8 @@ export fn prompt(prompt: *prompter, mode: mode) (void | io::error) = {
		yield "disclose";
	case mode::DELETE =>
		yield "delete";
	case mode::PERSIST =>
		yield "persist";
	})?;
};

-- 
2.45.2