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(-)
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 -3Learn more about email & git
--- 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
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
--- 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
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
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
--- 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
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
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
--- 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