You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

269 lines
11 KiB
Markdown

+++
title = "ciphey"
weight = 1
[taxonomies]
tags = []
+++
Simply put, `ciphey` is a password and secret manager that is like
[`pass`](https://passwordstore.org) if it used
[`age`](https://age-encryption.org) instead of PGP. More than anything, it is
an experiment to determine how cryptography can be combined in a minimalist
manner to protect passwords in a way that accounts for the most realistic
threats to their confidentiality, reliability, usability, and resiliency. It
takes many of the database and key management ideas from
[1Password](https://1password.com) and [Bitwarden](https://bitwarden.com) and
combines them with the Unix-like philosophy of `pass`.
To be clear, `ciphey` is still in development. [The remaining work is outlined
below](#remaining-work). While I will be using it as my actual password manager,
I strongly recommend that you only experiment with it and not rely on it for any
actual passwords.
*NOTE: This page is a work in progress.*
## The Basics
My goal is for `ciphey` to be simple enough that someone with a bit of knowledge
about cryptography who wants to understand how their passwords are protected can
do so after a short explanation. This sections aims to be that explanation.
### Entries
Everything you store in `ciphey` goes into an entry. Generally, you'll have one
`entry` per password. However, passwords come in many different forms: as a
result, entries are plaintext, which means any file can be used as an entry.
This doesn't mean there is no structure to entries. For example, take a look at
the entry for a hypothetical [`xkcd.com`](https://xkcd.com) account below:
```
correct horse battery staple
name: xkcd.com
tag: comics
username: Tr0ub4dor&3
url: https://xkcd.com/936
To anyone who understands information theory and security and is in an
infuriating argument with someone who does not (possibly involving mixed case),
I sincerely apologize.
```
The first line of every entry is interpreted as the entry's "secret." In this
case, the secret is `correct horse battery staple`, the password for the
account. This is usually a password or private key, but can be any value that
you might want to access by default. If this doesn't work for you for some
reason, no worries; just leave the line blank.
The secret is followed by any number of optional field-value pairs. Every
field-value pair lives on its own line and can contain any information you want,
so long as it follows the format `FIELD: VALUE`. Fields can be repeated and in
any order. The only limitation is that your `FIELD` cannot contain a `: ` (`:`
followed by a space), but the `VALUE` can.
There are no `FIELD`s with any special meaning (yet), but keeping them
consistent will help you find entries more quickly. I strongly recommend giving
each entry a `name` field, as this is what `ciphey` uses to search for entries
by default.
Finally, the contents of the file from the first line that doesn't match the
pattern `FIELD: VALUE` down to the end of the file is left uninterpreted,
meaning that you can save any text that you'd like there. This is called the
"note." In the example above, the "note" is the caption text from
[xkcd.com/936](https://xkcd.com/936). Separate the notes and the preceding
section with a newline.
### The Entries
Your `ciphey` database is a folder of files that can live on any filesystem.
This folder is completely encrypted, and thus can safely be backed up, shared,
or otherwise accessed without any unauthorized party gaining access to the
contents of the file.
```
ciphey/
└── entries/
├── age1sdnql3ksstj7krh7azddygh860f6uttus5t3e9j52t4k5z34kutqyzgr4e.age
├── age1ryutmsg486w4y06kq7w0rqm2s08r3sgzlwcl9p9p00jducedevvqkgrvyw.age
├── age1mqr5htmadgzlmcdjks79d6ucfe5k46ruavnaxm08m64lpl6h5udqhkwwvp.age
└── age1r908d20yj3ntp9q8g0448kwflfgf3au2qmhkkx7zp8xnesfdjfhqpd3r2d.age
```
Each file in the "entries" folder contains a single "entry." The name of each
file is the `age` public key the entry is encrypted to. Every entry into your
database is encrypted to a unique public key. Without that public key's
corresponding private key, the contents of the entry cannot be accessed.
However, keep in mind that some metadata does leak. Namely, any data associated
with the filesystem (date created, date last modified, owner/group, etc.) is NOT
protected by `ciphey` by default. It's still a good idea to keep your database
away from any curious eyes when it's avoidable.
### Your Keys
Every time you unlock your `ciphey` database, you must have two things:
1. Your `Primary Passphrase` (something you know)
2. A `Device Key` (something you have)
Every "device" you have (i.e. your desktop, laptop, and phone) has its own
`Device Key`. This key is generated and stored as a normal file when you first
set up `ciphey` on the device, and never leaves it.
Each `Device Key` is encrypted with your `Primary Passphrase`. Without your
`Primary Passphrase`, the `Device Key` cannot be used.
Alternatively, you can use a `Device Key` stored on a hardware token, such as a
Yubikey or OnlyKey. This has the added benefit that the key material never
leaves the hardware token, protecting against attacks that can steal key
material from your computer's local disk. In order to decrypt your entries, an
attacker would have to physically have access your hardware token, in addition
to learning your `Primary Passphrase`.
### Entry Keys
Every entry in your database is encrypted to a specific `Entry Key`. These entry
keys are stored next to the `database` directory in the `keys` directory. Every
entry gets its own key so that access can be cryptographically restricted on an
entry-by-entry basis.
The keys stored on the filesystem are encrypted to every `Device Key` that
you've given access to the entry.
### Disaster Recovery
When you set up your `ciphey` database for the first time, you'll be prompted to
create a `Recovery Key`, which is an `age` key encrypted with your `Recovery
Passphrase` (By default, your `Primary Passphrase`, but this can optionally be
changed). This key should be written down and stored in a safe place, such as in
a safe. This way, if you lose all of your devices, you can still regain access
to your passwords.
You can also give a copy of this key and your `Recovery Passphrase` to anybody
else who should have access to your accounts in case of an emergency. This way,
you can be sure that you're unlikely to ever lose access to your `ciphey` keys.
### Summary
Every entry is encrypted with its own unique `Entry Key`.
`Entry Key`s are encrypted to each `Device Key` that is granted access to the
entry. They can be encrypted to other people's `Device Key`s to share an entry
with them.
`Device Key`s are encrypted with your `Primary Passphrase`. They can either live
on your filesystem or on a hardware token, like a Yubikey.
When you first set up `ciphey`, you also create a `Recovery Key`, which every
`Entry Key` is encrypted to by default. This `Recovery Key` is encrypted with
your `Recovery Passphrase`, which are to be stored offline in a safe place
and/or given to trusted individuals so they can be used in case of an emergency.
### Usage
## Implementation
### Entry Parsing
The entry format was designed to be easy to parse for both humans and programs.
The following Rust snippet is a (simplified) example of parsing an Entry from a
String `s`:
```rust
// The entry's secret
let mut secret: String;
// A vector of key/value pairs
let mut fields: (String, String) = Vec::new();
// The entry's notes
let mut notes: Option<String> = None;
// An iterator over the decrypted lines of the entry file
let mut lines = s.lines();
// The first line contains the secret
secret = lines.next();
while let Some(line) = lines.by_ref().next() {
if let Some((key, value)) = line.split_once(": ") {
// Add the key and value to fields
fields.push((key, value));
} else {
// Take the rest of the lines and put them into notes
notes = Some(
std::iter::once(line)
.chain(lines.by_ref())
.collect::<Vec<String>>()
.join("\n"),
);
}
}
```
This particular implementation has a lot of room for improvement. The biggest
issue adding complexity is that Rust's `take_while` iterator method is
consuming, not peekable, meaning that ownership of the lines it checks is passed
to its closure. Thus, the following snippet, which is much easier to
read, would not work:
```rust
// An iterator over the decrypted lines of the entry file
let mut lines = s.lines();
// The first line contains the secret
secret = lines.next();
// Take lines while they can be parsed as key/value pairs split by ": "
fields = lines.take_while(line.split_once(": ")).map(...);
// first line failing take_while is incorrectly dropped here
// Join the remaining lines as notes
notes = lines.collect::<Vec<String>>().join("\n");
```
This is because even though the first line that fails the conditional is
consumed by `take_while`, it would neither be added to the fields (as it cannot
be parsed as one) nor collected into the vector over the lines of notes, instead
being dropped, causing data loss.
There are a few Rust crates that provide a peekable `take_while`, but they're
too big to justify as adding as dependencies for just this feature. It's
possible that a simpler approach is possible using an iterative method, but I
have not been able to come up with one yet.
## Remaining Work
### Password History
At the moment, there is no built-in support for keeping track of entry changes
over time. `pass` uses git to keep track of changes at the filesystem level,
while password managers like Bitwarden have built-in support for storing the
history of passwords over time.
Currently, there is nothing stopping someone from manually checking entries into
git or another backup solution. However, one way to provide better integration
into `ciphey` would be to allow for `pre-hooks` and `post-hooks`. These hooks
are scripts that are run before or after specific actions are taken by `ciphey`.
This would allow, for example, automatically committing the changes to a git
repository and pushing those changes to a remote server after every change made
by `ciphey`.
### File storage
For the moment, entries must be plaintext files. If you want to encrypt an
arbitrary file with `ciphey`, you have to manually create an encryption key,
store that key as an entry in `ciphey`, and then manually encrypt said file with
the newly-created key.
It would be beneficial to devise a way to treat encrypted files just like any
entry so that the key material for other files can be automatically managed in
the same way.
### Backup
While `ciphey` makes sure that you don't lose your secret key material through
the use of `Recovery Key`s, `ciphey` does not have any built-in backup system
for the encrypted `ciphey` database. Of course, you can still back up `ciphey`
like any other directory (which is a benefit of it living as just a plain
directory), but it may be useful to consider ways that `ciphey` can better
integrate or accomodate backups.