new(project): Ciphey
parent
64e06d062d
commit
fe483e896c
@ -1,6 +1,268 @@
|
|||||||
+++
|
+++
|
||||||
title = "Ciphey"
|
title = "ciphey"
|
||||||
weight = 1
|
weight = 1
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tags = []
|
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.
|
||||||
|
Loading…
Reference in New Issue