Bitwarden password manager

Here I’ll describe how Bitwarden works, and see whether I’m comfortable recommending it to others. Not that I trust myself to judge whether they’ve implemented their crypto correctly, but I can check for more obvious malfeatures.

Personally I use Lockbox & Locksmith on elementary OS, but I don’t know whether I can recommend those to anyone on other platforms or with multiple devices.

Certainly, I heavily recommend installing a password manager!


I’m focusing on Bitwarden Desktop specifically, but based on a skim all the other clients share most of the same code.

I’m specifically not studying the serverside, since Bitwarden’s security should not rely on the server before I can vouch for it.

Bitwarden Desktop is a Electron/Angular UI which starts by locating OS-specific filepaths.


Once it configures the logging, internationalization, & storage “services” to use those OS-specific filepaths it open the main window UI to show a spinner until the Angular controls are fully initialized.

It initializes the “messaging service” to dispatch various operations (scheduleNextSync, updateAppMenu, minimizeOnCopy, show/remove/hideTray, add/removeOpenAtLogin, setFocus, & enable/disableBrowserIntegration) to the right object & periodically send checkSyncVault messages.


It creates an alert-based UI upon Electron’s autoupdate infrastructure. (@zens can tell you why I hate this)

It creates a semi-platform-specific menubar with options to change the master password, enable two-step or fingerprint login, logout, copy usernames/passwords/verification codes, buy “premium membership” on Windows, various project links, add new logins etc, sync & export the vault, search, generate passwords, and more standard options.

It auto-logsout upon idle.


Next Bitwarden Desktop customizes how the app appears in your dock/process tray, helping it to stay in the background more.

Bitwarden initializes OS-specific biometric security if you want to authenticate that way. It integrates your Mac, Windows, or POSIX keychain via keytar to sync any passwords you’ve stored there.

It finishes off this chunk by initializing the messaging to the mainwindow, reading described earlier.


To “bootstrap” it first initializes keytar & then the main window. Once the mainwindow’s initialized it initializes the internationalization service, messaging, main menu, the corresponding item in the dock, etc to add a “lock now” option, the power monitor, updator, optionally biometric authentication.

It registers as the default handler for bitwarden:// URLs, & integrates hide/minimize/open-url events into the messaging service.

UI

When that main window’s opened, Electron’s configured to have it run the Angular application for that to message the backend I’ve just described. I don’t think I’ll go into too much detail here, I’ve said enough & this probably isn’t different from what you experience as a user.

There’s a service for sending messages back to the main window and other independant components.


The UI logic generic to all the browser extension, mobile, & desktop frontends! Much, but not all, of the actual presentation is rewritten for each of those.

It stores a hash of your master password to check it before using it to decrypt your vault.

There’s lots of TypeScript enum & interface types defined.

I’m carefully auditting their use of Google Analytics, which appears to be used to determine which platforms to support & which screens are viewed.


Much of the shared code between these frontends is there for thorough runtime & (Typescript) compiletime typechecking of the extensive serverside APIs for the various types of “secrets” can be sync’d & numerous ways you might want to authenticate into it.

As far as I can tell everything uploaded is encrypted, and the master password serving as the crypto key is pre-hashed before being sent.

It generates/stores a unique ID for Google Analytics, notifications, & authentication.


It integrates HaveIBeenPwnd to monitor breaches. It sends them your password hash prefixes (which is secure) to see if it’s been leaked, and it subscribes via the Bitwarden to be subscribed of the latest events.

It can import/export to/from most other password managers (I haven’t read it).

It’s got a password generator that first randomly chooses where any required characters should land then selects random ones. Or it selects n random words. zxcvbn computes pasword complexity.

zxcvbn

Many if not most online account systems you register onto are incompotent. They don’t always hash your passwords to protect against the inevitable break-in, and they don’t always do so correctly (Adobe Creative Cloud was an amusing example).

And those rules around how many symbols, numbers, uppercase, & lowercase characters must be in your password? “133tspeak” is consistant enough those are a worthless nuisance!

Dropbox’s zxcvbn checks some better password complexity hueristics.

I do have to bring up that zxcvbn is quite US-centric, it doesn’t even include localizations for it’s textual feedback! And it incorporates US popculture (which, to be fair, they like to export) into it’s hueristics. But at least it doesn’t complain about “foreigners’” weak passwords!

Bitwarden incorporates zxcvbn because a weak masterpassword undoes all it’s security measures!


There are 3 passes: lex the password, choose most guessable token sequence, & estimate number of guesses.

I do have to bring up that zxcvbn is quite US-centric, it doesn’t even include localizations for it’s textual feedback! And it incorporates US popculture (which, to be fair, they like to export) into it’s hueristics. But at least it doesn’t complain about “foreigners’” weak passwords!

Bitwarden incorporates zxcvbn because a weak masterpassword undoes all it’s security measures!


There are 3 passes: lex the password, choose most guessable token sequence, & estimate number of guesses.

The tokenization phase runs different passes to extract all occurences in a password of:


Each of those has it’s own logic for computing how guessable it is, assuming the attacker knows which dictionary or strategy you sourced it from.

It may be ambiguous which of those tokens applies for each substring, so it computes the non-overlapping sequence which takes the fewest guesses.

It computes this optimal subsequence by iterating over each start character & selecting a candidate token to take it’s place. Later tokens within it’s range may remove it. Any text not covered by taken is labelled as requiring bruteforcing.

The number of guesses required for previous tokens is incorporated into the estimated count for future ones.


The last two passes converts the results into a 0-4 score & English text describing how long it’d take to crack & how to improve your password.

GNOME/Linux Equivalents

libpwquality

libpwquality is a library originally from KDE wrapping Cracklib with some additional & configurable tests, which elementary OS integrates into all their user accounts screens. It serves as a C alternative to zxcvbn.

It can also generate random passwords, but the GUI tool I use “Locksmith” does that bit itself.

Checking/scoring a password is done by calling pwquality_check with some parsed configuration.


pwquality_check starts by normalizing it’s (empty) input, before running password_check, optionally CrackLib, & password_score.

password_check converts it’s input into lowercase before checking for:

All these checks can be configured or disabled via /etc/security/pwquality.conf. Most are by default.

And the code only deal in ASCII (or Latin1), so still seems US-centric…


To score a password, it starts by giving 2 points to every character beyond the minimum length.

Every unique character gets another point.

Presence of any digits, uppercase, lowercase, & symbols each contributes an additional 2 points.

The score is multiplied by 100/(3MIN_LENGTH + 2NUM_CLASSES). NUM_CLASSES = 4, for upper, lower, digit, & symbol.

50 points are dedacted, and the score is brought into hte range 0-100.


CrackLib starts by checking whether the password is too short. Then it checks how many different characters are present. It lowercases the password & trims surrounding spaces. If that yields an empty password, it’s rejected. Sequences are rejected. (US?) National Insurace numbers are rejected. Passwords based on account information are rejected, whether l33tspeak or not. Then it checks it’s leaked passwords database, under all l33tspeak interpretations.

Carefully avoiding malicious input.

GNOME Keyring

A technology which I arguably should have integrated into Odysseus, but never prioritized: GNOME Keyring. However I do use it manually via Lockbox.

This daemon exposes a DBus API (standardized by FreeDesktop.Org & shared with KWallet) which carefully authorizes access to stored “secrets”. e.g. usernames/passwords.

This job makes GNOME Keyring a tempting target for attackers, so it starts by dropping unneeded privileges, setting DBUS_FATAL_WARNINGS & checking G_DEBUG.


Only after it locked down it’s hatches does GNOME Keyring initialize it’s GLib, Gettext, & through a wrapper Libgcrypt (used primarily to selectively disable swapping memory to disk, because attackers with physical access to your harddrive can retrieve even overwritten data).

It configures logging to also go to syslog & parses the commandline flags. Possibly reading the user’s password carefully from stdin.

It finds/creates the directory in which to store secrets under it’s care.


It then creates the GLib mainloop, a control socket possibly sourced from systemd exposing an older non-DBus non-standard API, it might startup an SSH component, initializes DBus including quiting when the user logsout, prints some debugger, does the daemonization dance required by init systems other than systemd, configures the SIGPIPE, SIGTERM, SIGHUP, & SIGUSR1 interrupts, & reconfigures logging to also go to syslog.

Within the daemonized process it initializes the certificate & DBus API.


The certificate store reads data from multiple sources, but I’d rather focus on the DBus API. Once these are initialized the daemon runs it’s mainloop & cleans up.

It quits once the DBus connection closes. One API exposes the daemon’s “control directory” & environment variables. It tells the “session” to duplicate the daemon’s environment variables whenever those change. It more thoroughly watches for logouts. And that standard Secrets API is discussed next.

All in-memory secrets are cleared.


Turns out the actually initialization happens slightly elsewhere, the one with the rest of DBus initialization is for some reason essentially a noop as far as I can tell. It’s implementation takes greater advantage of the nicities of GLib’s DBus API.

The central service stores a bunch of “aliases” both in ~/.config/keyrings/ (or legacy ~/.gnome2/keyrings/) & in memory as a hashmap.

There’s a method for “opening” new Secret Sessions. Another linearscans all secrets on disk.


There’s a method for reading (metadata of) all stored secrets, which is what Lockbox shows me. This first does some access control checks, creates a PKCS11 session (mostly implemented in the external GCK module) for the client if missing, and uses that session to read all secrets matching the given criteria.

There’s a method for creating new secrets, which incorporates a seperate class which prompts the user for permission or additional info.

Upon “lock” it frees all sensitive information.


Unlocking a collection my require similar confirmation to saving a new secret.

Variants of these methods accepts a master password to symmetrically encrypt the secret with.

Once you’ve got a reference to a secret you can access the actual secret itself, edit it, or delete it. Though require confirmation dialogs.

The format on disk resembles that sent over the DBus socket.

Dependencies

GCK parses the bytes provided by P11Kit, taking care to not to leak it to disk via swapping. GCR implements an Object Oriented API around cryptography. Egg manages the non-swappable memory, amongst other simpler crypto tasks.

P11Kit provides loadable modules & dispatch tables for performing crypto accross a wide variety of direct & indirect backends. The exact functioning depends on configuration, and I’m failing to comprehend this code.