Alexey Yerin: 1 Implement dynamic store unlocking with the new prompter protocol 6 files changed, 135 insertions(+), 56 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/33024/mbox | git am -3Learn more about email & git
--- cmd/himitsud/cmd.ha | 79 +++++++++++++++++++++++++++++++++++++------ cmd/himitsud/main.ha | 39 --------------------- prompt/prompter.ha | 48 ++++++++++++++++++++++++-- secstore/secstore.ha | 11 ++++-- secstore/serialize.ha | 2 +- secstore/types.ha | 12 +++++++ 6 files changed, 135 insertions(+), 56 deletions(-) diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha index efa4383..3f160e6 100644 --- a/cmd/himitsud/cmd.ha +++ b/cmd/himitsud/cmd.ha @@ -74,15 +74,21 @@ fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = { }; fn exec_add(serv: *server, client: *client, args: []str) (void | cmderror) = { + if (serv.store.state != secstore::state::UNLOCKED) { + const prompter = prompt::newprompter(serv.conf.prompter[0], + serv.conf.prompter[1..])?; + prompt::unlock(&prompter)?; + if (!prompt::wait_unlock(&prompter, serv.store)?) { + writefmt(client, "error Failed to unlock"); + return; + }; + prompt::close(&prompter)?; + }; + const q = query::parse_items(args[1..])?; defer query::finish(&q); // TODO: Prompt user to fill in incomplete keys - let entry = match (secstore::add(serv.store, &q)) { - case secstore::locked => - abort(); // TODO - case let entry: *secstore::entry => - yield entry; - }; + let entry = secstore::add(serv.store, &q)!; let buf = bufio::dynamic(io::mode::WRITE); fmt::fprint(&buf, "key ")?; secstore::write(serv.store, &buf, entry, false)?; @@ -96,6 +102,17 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = { let buf = bufio::dynamic(io::mode::WRITE); + let prompter: (prompt::prompter | void) = void; + if (serv.store.state != secstore::state::UNLOCKED) { + const new = prompt::newprompter(serv.conf.prompter[0], + serv.conf.prompter[1..])?; + prompt::unlock(&new)?; + if (!prompt::wait_unlock(&new, serv.store)?) { + writefmt(client, "error Failed to unlock"); + return; + }; + }; + const iter = secstore::query(serv.store, &q); let matches: []*secstore::entry = []; for (true) { @@ -112,8 +129,13 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = { }; if (len(matches) > 0) { - const prompter = prompt::newprompter(serv.conf.prompter[0], - serv.conf.prompter[1..])?; + const prompter = match (prompter) { + case let p: prompt::prompter => + yield p; + case void => + yield prompt::newprompter(serv.conf.prompter[0], + serv.conf.prompter[1..])?; + }; for (let i = 0z; i < len(matches); i += 1) { prompt::sendkey(&prompter, serv.store, matches[i])?; }; @@ -123,7 +145,13 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = { return; }; - secstore::del(serv.store, &q)?; + secstore::del(serv.store, &q)!; + } else { + match (prompter) { + case let p: prompt::prompter => + prompt::close(&p)?; + case void => yield; + }; }; fmt::fprintln(&buf, "end")?; @@ -147,6 +175,21 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = { }; }; + let prompter: (prompt::prompter | void) = void; + if (serv.store.state == secstore::state::HARD_LOCKED) { + const new = prompt::newprompter(serv.conf.prompter[0], + serv.conf.prompter[1..])?; + prompt::unlock(&new)?; + if (!prompt::wait_unlock(&new, serv.store)?) { + writefmt(client, "error Failed to unlock"); + return; + }; + if (!decrypt) { + prompt::close(&new)?; + }; + prompter = new; + }; + const q = query::parse_items(cmd.args)?; defer query::finish(&q); @@ -163,12 +206,26 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = { }; if (len(matches) > 0 && decrypt) { - const prompter = prompt::newprompter(serv.conf.prompter[0], - serv.conf.prompter[1..])?; + const prompter = match (prompter) { + case let p: prompt::prompter => + yield p; + case void => + yield prompt::newprompter(serv.conf.prompter[0], + serv.conf.prompter[1..])?; + }; for (let i = 0z; i < len(matches); i += 1) { prompt::sendkey(&prompter, serv.store, matches[i])?; }; + if (serv.store.state == secstore::state::SOFT_LOCKED) { + prompt::unlock(&prompter)?; + }; prompt::prompt(&prompter, prompt::mode::DISCLOSE)?; + if (serv.store.state == secstore::state::SOFT_LOCKED) { + if (!prompt::wait_unlock(&prompter, serv.store)?) { + writefmt(client, "error Failed to unlock"); + return; + }; + }; if (!prompt::wait(&prompter)?) { writefmt(client, "error User declined"); return; diff --git a/cmd/himitsud/main.ha b/cmd/himitsud/main.ha index 66d27d8..f89355d 100644 --- a/cmd/himitsud/main.ha +++ b/cmd/himitsud/main.ha @@ -1,5 +1,3 @@ -use bufio; -use bytes; use config; use fmt; use getopt; @@ -10,7 +8,6 @@ use os; use rt; use secstore; use unix::signal; -use unix::tty; use unix; export fn main() void = { @@ -29,48 +26,12 @@ export fn main() void = { }; }; - const tty = match (tty::open()) { - case let file: io::file => - yield file; - case let err: tty::error => - fmt::fatal("Error opening tty:", tty::strerror(err)); - }; - - const termios = tty::termios_query(tty)!; - tty::noecho(&termios)!; - - // TODO: There should be more convenient ways of starting himitsud - // TODO: Better manage secure memory with bufio::scanline/mlock/etc - fmt::error("Please enter your passphrase to unlock the keyring: ")!; - const pass = bufio::scanline(tty)!; - tty::termios_restore(&termios); - - const pass = match (pass) { - case let buf: []u8 => - yield buf; - case io::EOF => - fmt::errorln()!; - fmt::fatal("Error: no passphrase supplied"); - }; - fmt::errorln()!; - const store = match (secstore::open()) { case let err: secstore::error => - bytes::zero(pass); fmt::fatal("Error opening secstore:", secstore::strerror(err)); case let store: secstore::secstore => yield store; }; - const err = secstore::unlock(&store, pass); - bytes::zero(pass); - free(pass); - - match (err) { - case let err: secstore::error => - fmt::fatal("Error opening secstore:", secstore::strerror(err)); - case void => - yield; - }; defer secstore::close(&store); signal::block(signal::SIGINT, signal::SIGTERM); diff --git a/prompt/prompter.ha b/prompt/prompter.ha index cd5889b..cdf8542 100644 --- a/prompt/prompter.ha +++ b/prompt/prompter.ha @@ -1,3 +1,4 @@ +use bytes; use bufio; use errors; use fmt; @@ -72,6 +73,11 @@ export fn sendkey( fmt::fprintln(prompt.stdin)?; }; +// Sends an unlock request to the prompter. +export fn unlock(prompt: *prompter) (void | error) = { + fmt::fprintln(prompt.stdin, "unlock")?; +}; + // Sends a "prompt" command to the prompter with the given operating mode. export fn prompt(prompt: *prompter, mode: mode) (void | io::error) = { fmt::fprintfln(prompt.stdin, "prompt {}", switch (mode) { @@ -80,13 +86,45 @@ export fn prompt(prompt: *prompter, mode: mode) (void | io::error) = { case mode::DELETE => yield "delete"; })?; - io::close(prompt.stdin)?; - io::close(prompt.stdout)?; +}; + +// Waits for a password from the prompter and unlocks the store. +export fn wait_unlock( + prompt: *prompter, + store: *secstore::secstore, +) (bool | error) = { + for (true) match (bufio::scanline(prompt.stdout)?) { + case io::EOF => + break; + case let buf: []u8 => + defer { + bytes::zero(buf); + free(buf); + }; + const string = strings::fromutf8_unsafe(buf); + + const (cmd, pass) = strings::cut(string, " "); + if (cmd != "password") { + return protoerror; + }; + match (secstore::unlock(store, strings::toutf8(pass))) { + case void => + fmt::fprintln(prompt.stdin, "password correct")?; + return true; + case secstore::badpass => + fmt::fprintln(prompt.stdin, "password incorrect")?; + case let err: secstore::error => + abort("Unexpected secstore error"); + }; + }; + return false; }; // Waits for the prompter to complete and returns true if the user consented to // the operation, or false otherwise. export fn wait(prompt: *prompter) (bool | error) = { + close(prompt)?; + const res = exec::wait(&prompt.proc)?; match (exec::check(&res)) { case void => @@ -107,3 +145,9 @@ export fn wait(prompt: *prompter) (bool | error) = { }; }; }; + +// Closes standard input and standard output of a prompter. +export fn close(prompt: *prompter) (void | error) = { + io::close(prompt.stdin)?; + io::close(prompt.stdout)?; +}; diff --git a/secstore/secstore.ha b/secstore/secstore.ha index 2eb2be0..a2884dc 100644 --- a/secstore/secstore.ha +++ b/secstore/secstore.ha @@ -62,6 +62,7 @@ export fn create(passphrase: []u8) (secstore | error) = { return secstore { key = key, + state = state::UNLOCKED, dir = dir, index = index, entries = [], @@ -78,6 +79,7 @@ export fn open() (secstore | error) = { const index = os::open(path::string(&buf), fs::flags::RDWR)?; return secstore { key = void, + state = state::HARD_LOCKED, dir = dir, index = index, entries = [], @@ -161,6 +163,7 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = { }; store.key = keystore::newkey(key[..32], "secstore")!; + store.state = state::UNLOCKED; match (load_index(store)) { case void => yield; case let err: io::error => @@ -170,20 +173,22 @@ export fn unlock(store: *secstore, passphrase: []u8) (void | error) = { }; }; -// Locks the key store, unloading the decryption keys and evicting entries. +// Locks the key store, unloading the decryption keys. export fn lock(store: *secstore) void = { match (store.key) { case let key: keystore::key => keystore::destroy(key); + store.key = void; case void => yield; }; - free_keys(store); + store.state = state::SOFT_LOCKED; }; // Closes the secstore, freeing any associated resources. export fn close(store: *secstore) void = { lock(store); + free_keys(store); io::close(store.index)!; free(store.dir); }; @@ -197,7 +202,7 @@ fn free_keys(store: *secstore) void = { // Adds an item to the keystore. The provided query must not have any missing // values. export fn add(store: *secstore, q: *query::query) (*entry | locked) = { - if (store.key is void) { + if (store.state != state::UNLOCKED) { return locked; }; diff --git a/secstore/serialize.ha b/secstore/serialize.ha index 2d8dab4..43e6676 100644 --- a/secstore/serialize.ha +++ b/secstore/serialize.ha @@ -21,7 +21,7 @@ export fn write( ent: *entry, private: bool, ) (void | error) = { - if (store.key is void) { + if (private && store.state != state::UNLOCKED) { return locked; }; diff --git a/secstore/types.ha b/secstore/types.ha index 2039af4..79d42bf 100644 --- a/secstore/types.ha +++ b/secstore/types.ha @@ -4,8 +4,20 @@ use fs; use io; use uuid; +export type state = enum { + // The store is fully unlocked + UNLOCKED, + + // Partially locked - only secret values are encrypted + SOFT_LOCKED, + + // Fully locked - all key/value entries are encrypted + HARD_LOCKED, +}; + export type secstore = struct { key: (keystore::key | void), + state: state, dir: str, index: io::handle, // TODO: Consider adding hash table or btree or something. Might be fun -- 2.36.1
I don't love all of the match statements on the prompter, but we can address that later. Thanks! To git@git.sr.ht:~sircmpwn/himitsu 1bfc63a..d57dd69 master -> master