~ser/rook

A secret service tool backed by a KeePass v4 database
~ser/rook

New ticket tracker added

2 months ago
~ser/public-inbox

New mailing list added

2 months ago

#Rook

logo

Build statusReleasesChangelogIssuesGo Report Card

A lightweight, stand-alone, headless secret service tool backed by a Keepass v2 database.

Rook allows you to use a KeePass v2 database as storage for secrets. It provides client and server modes; the server unlocks the database and stays in memory; the client communicates over a socket with the server and fetches data. This allows:

  • Using your KeePass database, instead of having to have all your secrets also stored in a second system
  • Allows you to unlock the database once, instead of every time the system needs a secret
  • Allows you to use your secrets in scripts and tools such as mbsync/isync, vdirsyncer, offlineimap, msmtp, encrypted file system mounters, encrypted and/or offsite backup programs, IMAP email clients, and so on.
  • Allows you to run a headless secret server
  • Is easily auditable, and as a single binary is more easily protectable

There are other ways of accomplishing each of these, but none that accomplish all of them. passhole and kpmenu are close alternatives. passhole is Python with a lot of dependencies to audit. Recent versions of kpmenu also have large dependency trees, and the mode of operation makes it harder to use for non-GUI scripting.

Rook is not compatible with any other secret service tools; it does not communicate over, or rely on, DBus, or anything systemd. It should work as well on Darwin, *BSD, or any other POSIX-ish OS as it does on Linux, although this has not been tested. I don't know what it'll do on Windows.

#Status & Features

Rook is mostly complete. Some features are missing; see the Roadmap section below. It might crash, although it's been a while since I've seen one. It should be currently able to replace most secret-service type calls you might make from scripts; you can easily fetch passwords by account with the client, which is what you really need.

  • Read kdbx v2 Keeass database files
  • List entries from the DB
  • Search entries, by title, URL, or tag
  • Show entries
  • Show individual fields, such as username & password
  • Reload a (presumably changed) database; open a different database
  • Lock / unlock a database
  • Pin-controlled client calls for additional security (see Secure section, below)
  • Headless
  • Minimal dependencies and an auditable amount of sourcecode -- you can check to make sure it's not doing anything fishy, even if you aren't a Go expert. This repo contains around 1k LOC in 5 files. Assuming you trust the core Go team, you only then have to audit two other external libraries: a flags lib written by me that is a single Go file with no external dependencies and which has no networking code; and the library that provides access to the KeepassDB (gokeepasslib has itself one external dependency, a crypto library written by the same author) - neither of which have networking code.

#Usage

rook help prints a list of the supported commands. Start a rook server process with rook serve, and then access it by calling rook again with one of the other commands. There is no configuration, or configuration file.

Exit the rook server with rook exit or ^c. If, for some reason, it leaves behind the socket file, the next time you start rook it'll complain about one already running. You can override that warning with the -f argument.

Rook will watch the DB file and auto-refresh the memory cache if the file changes.

Start a rook server with:

rook -f serve -P /path/to/your.kdbx

-f here forces rook to ignore and overwrite any pre-existing socket file; you probably shouldn't use it unless you are certain rook isn't already running. -P tells rook to prompt for the database password. -P can be used either when starting a server, or when calling open from a client. Alternatively (and, probably, more normally), start the rook server with no password, and unlock it with a client call when you can prompt for a password:

$ rook serve /path/to/kdbx &
$ rook open -P

Or, the server can be called with no database, and the client can tell it which database to open by passing it a path; in this case, it must be a path the server can find the file through, usually a fully qualified path. However, if the rook server was run in the same directory as one or more databases, the client can provide a simple relative path:

$ rook open -P my.kdbx

A database supplied by open replaces whatever the server was started with.

As an alternative to all of password prompting, rook will also take a password from the ROOK_PASSWORD environment variable. The prompt takes precedence. Remember that passwords are plain text, and environment variables are -- while reasonably secure -- still more persistent and accessible than other mechanisms. The password may also be provided to the serve command via stdin pipe with the --stdin argument; this allows starting a Rook server with a password vault without exposing the password through environment variables and without requiring user interaction. This means you could also do this in an interactive bash, as an alternative to -P:

$ rook serve --stdin database.kdbx
supersecretpassword
^d

It's possible to tell whether the database is unlocked by making most any other request, e.g. info, as the server will return a NO OPEN DATABASE error message. If no database is open, the lock and commands requests will not throw errors, and the only other request that will work is open.

Databases can be live-reloaded with the reload command; this assumes the credentials have not changed,and it simply replaces the in-memory database from the current version on disk. There is not yet -- and may never be -- fancy DB merging such as KeepassXC does; it's a straight-up replacement.

If the entry Title is unique, then show only needs that; otherwise, it'll show all the matching entities -- which may not be what you want when using rook for script authentication purposes. In the case of duplicates, provide the path - separated by / - to the entity -- or part of it. For example, if you Keepass database looks like:

Root
  Internet
    Accounts
      me
  Server
    Accounts
      me
  Identities
    me
    myself

then rook show me will show of all three me entries; rook show Accounts me will show two of them; rook show myself will show only the one entry; and rook show Server/Accounts me will show only the one entry. A fully qualified path would be rook show Root/Internet/Accounts me, but rook requires only enough to uniquely identify entries.

Rook can also display your database as a tree by using the --tree argument:

$ rook ls -t
┌─ /
├─┬─ Subgroup  1
│ ├─┬─ Subsubgroup 2
│ │ └── Subsub item
│ └── Subgroup 1 item 1
├── First entry
├── Second test
├── Third entry
├── fourth entry
└── Something wicked

Empty groups are not shown; leaves in the tree are always entries.

If you want to move the socket location, e.g. to /var/run/user/$(id -u)/rook.sock then use the --socket argument. If you do this, you must set it for both the server and every client call. Easiest would be to create an alias in your shell's rc file, but you can also just specify it everywhere you call Rook.

#Caveats

rook list shows all entries in the DB, with an index number and a path. While the index can be used, it's not a fixed thing and could change as the database changes, so it's best to not rely on it in scripts.

#Examples

The alias in the following example isn't necessary; it's just to keep rook running everything in ${PWD}.

Here are some code snippets from various config files:

mbsync (~/.config/isyncrc)

PassCmd "rook show -p 'Email account'"

aerc (~/.config/aerc/accounts.conf)

source-cred-cmd = rook show -p Accounts 'Email account' 

vdirsyncer (~/.config/vdirsyncer/config)

password.fetch = ["command", "rook","show","-p","CardDAV account"] 

msmtp (~/.msmtprc)

passwordeval   rook show -p Identities "SMTP server"

restic You could store all of your restic backup information in Keepass:

RESTIC_PASSWORD=$(rook show -p Accounts/Backups glamdring) \
  RESTIC_REPOSITORY=$(rook show -f restic_repository Accounts/Backups glamdring) \
  EXCLUDES=($(rook show -f excludes Accounts/Backups glamdring)) \
  /usr/bin/restic backup ${EXCLUDES[@]/#/-e } --one-file-system \
  $(rook show -f files Accounts/Backups glamdring)

Searches look in several places for matches: titles, URLs, and tags. However, show only matches against titles. Both search and show also take an optional path as the first argument, which filters the entities before matching; e.g.:

$ rook ls
1    Accounts   Entry 1
2    Accounts   Entry 2
3    Internet   Entry 1
4    Internet   Entry 2
$ rook search 'Entry 2'
Index: 2
Path: Accounts
Title: Entry 2

Index: 4
Path: Internet
Title: Entry 2
$ rook search Internet 'Entry 2'
Index: 4
Path: Internet
Title: Entry 2

#Autotype

There are shell scripts in the utils/ directory: autotype.sh and getAttr.sh. autotype.sh can provide form-filling utility; it works by grabbing the focused window's title and uses rook match to find the best match in the DB -- rook uses the Keepass Autotype rules for this, if they exist -- and the best match is returned with the first line being the defined autotype sequence for the entry. The script then parses out the key/value pairs and uses xdotool to type the sequence. getAttr.sh uses rofi to allow the user to select an entry from the DB, and then an attribute from the entry, and then it performs a user-requested action with the data.

autotype.sh and getAttr.sh are shell scripts; if you use them, you should also read through them. They're not exactly trivial scripts, but at ~100 lines long each they're auditable even if you're not a shell expert. Again, I trust me implicitly, but you shouldn't.

autotype.sh is straightforward -- it just does its thing: tries to find the best entry and type the autotype sequence. If it gets multiple matches, it shows them in a rofi dialog, you choose one, and off it goes.

getAttr.sh has a -h argument for help, and more complex options. It can use fzf instead of rofi, and be used entirely from the command line. It can copy the selected value to the system clipboard, print the value out, or type the value out with xdotool.

#autotype & getAttr Dependencies

These are dependencies only for the utility scripts -- the rook binary itself does not use any of these.

  • zsh, because I just couldn't be arsed to do the extra work to make some things in autotype.sh work the harder way in bash.
  • ripgrep, because -- again -- it makes some things just so much easier.
  • xdotool, for teh tappy-tap-tap.
  • xprop, because Luakit needs hacks to work properly. If you don't use Luakit, you can (should?) delete the hacks -- it'll probably make things more robust.
  • yad, for providing info dialogs.
  • xsel, used in autoType.sh to put values in the clipboard.

KeepassXC autotype sequences have a lot of keywords; the script understands a small number of these -- ENTER, DELAY, TOTP, USERNAME, PASSWORD. It should be easy to add to this list by modifying the case switch at the end of the file -- improvement patches are welcome.

To use the script, put it in your path and bind it to a hotkey, e.g.

herbstluftwm keybind Mod4+t spawn ${HOME}/bin/autotype.sh
### OR, if you installed with a package manager:
herbstluftwm keybind Mod4+t spawn /usr/bin/rook-autotype

If you installed rook with one of the binary distribution packages, and you have the necessary required programs listed above (rofi, ripgrep, yad, etc.), then the utility scripts will be installed in /usr/bin/ as rook-autotype and rook-getattr.

#Select Entry Attribute

There's a shell script in utils/ called getAttr.sh which shows a list of all entries, and when one is selected shows a list of all attributes, one of which can be selected. If called with the -t argument, the selected value will be typed using xdotool. If called with the -c arg, the value will be copied into the clipboard using xsel. If no argument is given, the script prints the value to STDOUT.

getAttr.sh can prompt either with rofi or fzf; the -f argument selects fzf, and rofi is used by default. Using fzf allows getAttr to be used without a GUI.

#Testing

You can see how autotype.sh will work by faking an input dialog with yad. To get as close to real life, you'll want to run the application that will ask for the login -- if it's Paypal, then go to Paypal.com in an incognito window and click "Logn". If it's some GTK application, run the application and start the login process. Then use xprop to get the window title -- this is what autotype.sh, and incidentally, KeepassXC, uses for entry matching. Then run yad with that window title as the --title parameter -- something like this:

yad  --form --field "Username" --field "Password:H" --field "TOTP:NUM" --title "Log in to Sourcehut"

Give that Username field focus and trigger your autotype -- once you hit OK, the values will be printed on the command line.

#Install

#From pre-built binaries

Download the latest release from the releases page. Optionally, also download the binary signature and validate the binary. Decompress the binary with gzip, copy it somewhere in your path, and off you go.

I provide builds for Linux arm64 and amd64, and the latest version as RPM, deb, apk, and Arch zst packages for each of these.

You can get my public key either from Sourcehut, or from one of the public key servers. You'll want to grab the one that says:

Sean E. Russell (Signing key for Sourcehut builds) <ser@ser1.net>, which is key 5E0D7ABD6668FDD1. To get it from Sourcehut, where the builds are made:

curl https://meta.sr.ht/~ser.pgp | gpg --import
###      OR    ###
gpg --keyserver keys.openpgp.org --recv-keys 6668FDD1
###  AND THEN  ###
gpg --verify rook_arm64_latest.gz.sig

If you install from one of the pre-built packages, the script autotype.sh will be installed as /usr/bin/rook-autotype, and getAttr.sh as /usr/bin/rook-getattr.

#Build from source

If you want to try building and running on something other than Linux -- say, one of the BSDs, or Darwin -- then this or the [next section](#Install the Go way) is what you'll need to use.

You'll need Mercurial and Go

hg clone https://hg.sr.ht/~ser/rook
go build .
# OR, to embed the correct version s.t. `rook version` works,
go build -ldflags "-X main.Version=$(hg log -r tip --template '{bookmarks}')" .

#Install the Go way

go install ser1.net/rook@latest
# OR, if you know the version you want
go install -ldflags '-X main.Version=v0.0.9' ser1.net/rook@v0.0.9

#nfpm

The repo contains an nfpm.yaml file, from which can be built packages for Arch, rpm, apk, and deb systems. You could use this to build old versions, or versions for other architectures such as arm5/6/7. If you want to do this, you're best off reading the nfpm usage docs, since repeating them here would be silly. But, basically, you'll need to make sure some environment variables are set, and run (to get an apk; replacable with rpm, deb, or archlinux):

VERSION=$(hg log -r tip --template '{bookmarks}') \
  GOOS=$(go env GOOS) \
  GOARCH=$(go env GOARCH) && \
  nfpm pkg -p apk

#Security

Rook's threat model is simple: it doesn't expose anything to any network connection, but does trust that the local system is secure. Don't use Rook if you don't trust the root user, or worry that someone could masquerade as your user. If your user or root accounts are compromised, then an attacker can talk to the Rook server over the socket and get at your secrets. Since the socket is created with user-only RW permissions, and is created in the user's home directory, it should still be safe on multi-user systems -- with the aforementioned caveat that root has access to everything.

Rook attempts to keep external dependencies to an absolute minimum. Rook itself is not a large program, and is only multiple files for semantic encapsulation. Aside from the Rook code itself, dependencies are:

  • github.com/tobischo/gokeepasslib/v3, which is the library used to process the KeePass libraries. It should have no network access, and is easy enough to grep for net imports. It, itself, depends on
  • github.com/tobischo/argon2, which is a crypto library. Again, no network and easy enough to grep for net
  • golang.org/x/crypto, which I haven't audited, but I guess I trust the Go core team enough
  • golang.org/x/sys, same as above
  • ser1.net/claptrap/v4, my bespoke flags library. It has no net access, is entirely a single 4k-line file. I trust it implicitly, but again, it's easy enough to check to make sure it's not doing anything shady. Again, the same author who wrote rook also wrote claptrap (me).

So, in all of this, Rook should be the only thing using net, and would be the only thing (if I weren't me) I'd be concerned about. rook needs net to create the local socket over which the client/server communicate, and there are only two places rook opens a connection, both of which are to a local socket.

Regardless, as with any secret services, if you don't trust your own system, don't run these. Don't run KeePassXC at all, because with enough effort, root can still get memory dumps and extract secrets from your open database.

#Secure mode (--secure)

Security can be improved by running rook in secure mode, by starting the server with --secure. In this mode, the server will print out a 6-digit pin that the client will always have to provide for every interaction. Locking and unlocking the database will generate a new pin.

When run with --secure, whatever provides the database credentials will print out the pin; if the server is run with -P argument or with the ROOK_PASSWORD environment variable, then the server will print the pin out on STDOUT. If the DB is locked and unlocked, or if the client opens the database with the credentials, then the pin will be returned to the client. If the server is run in secure mode, the client will look in the environment variable ROOK_PIN for the pin; this is the only way to pass the pin to the client.

The secure mode unfortunately makes scripting less useful, as every automated script now has to prompt the user for the pin whenever a call is made, and either cache it somehow or prompt the user every time. This can easily make rook less secure, if one of the scripts -- for example -- writes the pin to a cache file. This can be bypassed by updating a session environment variable, although this (again) re-weakens security as environment variables can be read by both root and any session logged in as the user. Also note that setting environment variables through a shell will usually expose the pin in any persistent shell history file.

#(Auto)Locking

If you want the DB to be locked periodically, run an external cron job that calls lock. Rook might add a lock-after-inactivity function, since this can't really be done externally, but (a) it'd be useless to me since the purpose of rook is as a secret-service for other cron jobs like mail syncing, and so it'd never timeout and lock; (b) I'm really trying to limit the amount of code in rook, making it more auditable, and that means leaving out features; and (c) a general lock-after-time regardless of use is arguably more secure, and a more common feature in these sorts of tools -- and that can be done externally.

#Master password plaintext considerations

Rook itself does not keep the master password in memory in clear text, and neither does gokeepasslib. Variables that hold the password during credentialing are cleared, or are scoped out in brief functions.

Don't use the --password argument. It's there to simplify testing, nothing more. If you use it, it could easily be stored in a shell history file, or even worse, be seen by any user in the sysetem through ps.

Use of ROOK_PASSWORD for the server is discouraged. While any account that can inspect the rook process' environment (via /proc/<pid>/environ) can also probably dump memory and get at your secrets, by using the environment variable your master password is stored in plain text and this is less secure. It's less of a concern for client calls, because client calls are ephemeral.

The best way to minimize the master password's existence in memory in plain text is to run the rook server with at most the database path, and then use the open command with the client, e.g.:

$ rook serve database.kdbx
$ rook open -P

#Roadmap

Most of the roadmap is kept in the sourcecode. A good way of looking at it is to get legume and run leg in the source directory. However, there is a general thrust to most of what's coming:

  • Bug fixes
  • DB modification commands. I'm bearish about adding these: I think they're best done through tools like KeepassXC, which provide nice UIs, and which handle conflicts and merging well; they also add a lot of complexity that would make auditing harder, and for questionable value. Rook is not intended to fully replace KeepassXC tools. That said: small changes, like changing a password or adding TOTP to an entry, might be handy to do on the CLI. We shall see.

There are countless giants that should be listed, that we all take so much for granted that they are unknown to most of us. However, I'd like to call out a couple of individuals whose code I've leveraged heavily, or sometimes (basically) stolen. Those I've copied/pasted from I've also done my best to audit in the process.

  • tobischo, for whom I'm very grateful for allowing me to avoid reading the entire Keepass v2 spec and implement it by hand. Although I probably should have, in following with my own attempt to minimize the attack surface of rook.
  • jlinoff, whose getPassword (MIT) code filled a tedious hole. I very much appreciate that jlinoff not only provided a solution, but provided 3 solutions with different dependencies ranging from stdlib only, stdlib but with some of the more esoteric packages, and a final one that pulls in a golang.org/x package.
  • The rook logo was derived from SVG art titled 'Raven With Key' by Karen Arnold. The original was licensed CC0, and so, therefore, is this.

rook is developed using a set of decentralized, CLI-based tools. Web tools are fine, and they're sometimes necessary, but they centralize data, are hard to script, and just generally get in my way. When there are many people from different domains collaborating on a thing, this extra impediment is necessary; when there aren't, it's not. I use code comments for issue tracking and legume to make that easier to view; I use changelog to generate the CHANGELOG.md.

rook was inspired by kpmenu, which approaches security differently, requiring interaction from the user on every request. This makes it harder to use as a secret service for automating scripts, and since kpmenu has a very specific threat model it's been resistant to patches that would weaken their solution, and even with those patches additional tools are needed to support autotype. As mentioned in Security, Rook assumes the user environment is secure and can therefore take a more relaxed approach; these are the same assumptions and approaches used by any secret service software, such as Gnome's secret-tool, keyring, or pass.

#Other solutions

Linux users have many options for storing passwords. I can't list all of them here, but I can list the ones I've used, and why I believe rook was necessary. In every case where the tool has a bespoke data store, it means the user having to manually syncing passwords between Keepass and whatever the tool is using, and is for this reason is IMO unsuitable.

  • KeepassXC. This is an outstanding program that you should use. It does include a proper secret service option, in that it is compatible with keyring and Gnome's secret-tool. It is, however, a GUI tool, and the CLI program that comes with it requires a password every time it is called, making it unsuitable for background scripts.
  • kpmenu. As mentioned above, kpmenu's architecture does not lend itself to running as a secret service that can be used by background jobs needing credentials.
  • secret-service is another keyring-compatible solution. It uses its own backend DB store, and I want to use a Keepass DB. It also has a fairly large number of external, non-stdlib dependencies (19), making it much harder to fully audit.
  • secret-tool. This is what Gnome uses for password stores; it has a bespoke password database (not Keepass), and communicates over DBus -- using secret-tool outside of Gnome pulls in several extra services and software dependencies, which is relatively heavy if a secret service is all you're using it for.
  • keyring. This is (I believe) the CLI tool for KDE and KWallet; it is compatible with Gnome's secret service, again communicating over DBus. It requires the KWallet service to be running in the background, and IIRC that kicks off several related KDE services. Like secret-tool, if you're not running the KDE desktop, you're getting a chunk of the load of it by running keyring.
  • pass. git for passwords, encrypted with GPG! It's a fantastic idea, and by using gpg-agent it's not adding much to your service load. It has two downsides: first, it's still a bespoke backend password database, meaning having to manually sync any passwords or entries with Keepass by hand; and second, it exposes a huge amount of metadata about the secrets. Since the entire DB is a filesystem, where the entry titles are directory names and attributes are files in the directory, the entire structure of the database is exposed, unencrypted, and readable. Attackers may not know what your Pornhub password is, but they know you have an account. This is fine for pass, which is designed more for secret sharing across teams, and so the account names are not considered confidential information. It's pretty horrible for anyone else, and especially anyone considered a dissident for whom that metadata could be used in persecution.
  • gopass is a Go-based client for pass databases. If you use pass, it's a pretty nice tool. pass itself is a collection of shell scripts; gopass is a single self-contained binary. It doesn't have any connection with Keepass, though.