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 v3: Drew review (english wording, add "never" as choice, code style)
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..429968f 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