remove outdated projects
parent
05a34637a6
commit
8713cb4e86
@ -1,45 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Bamboo Media Server"
|
|
||||||
weight = 3
|
|
||||||
[taxonomies]
|
|
||||||
tags = []
|
|
||||||
+++
|
|
||||||
|
|
||||||
Bamboo is a self-hosted personal media server written in Rust that I've been
|
|
||||||
working on for the past two years with my friend
|
|
||||||
[Ersei](https://ersei.saggis.com). It aims to replace existing media servers
|
|
||||||
like Jellyfin and Plex by decoupling the front-end, API, and content as much as
|
|
||||||
possible, allowing for richer client applications and a wider range of content
|
|
||||||
and content types.
|
|
||||||
|
|
||||||
Most of what makes Bamboo's architecture different is its ability to generalize
|
|
||||||
over types and sources of content while providing common primitives for richer
|
|
||||||
metadata.
|
|
||||||
|
|
||||||
For example, Jellyfin provides a few distinct types of media that it can serve:
|
|
||||||
Movies, Music, Shows, Books, Photos, and Music Videos. Jellyfin's API is deeply
|
|
||||||
tied to its web front-end, so the content you can host on Jellyfin is limited to
|
|
||||||
what the Jellyfin web client is capable of displaying. This works okay for
|
|
||||||
classic media server content like Movies and TV, but even something slightly
|
|
||||||
beyond its expectations, like a YouTube video or Twitch stream, must be made to
|
|
||||||
conform to the formats that Jellyfin expects.
|
|
||||||
|
|
||||||
Bamboo solves this problem by defining generic types that are common to all
|
|
||||||
media, such as the Title, a UUID, and a list of URLs it can be accessed at, and
|
|
||||||
allowing specific types of media to expand upon with additional data and fields.
|
|
||||||
This way, clients can be as specific or general as they desire based on their
|
|
||||||
required functionality. A media search application may not need to care about
|
|
||||||
anything beyond the title, while a Podcast application should only accept media
|
|
||||||
that is of the Podcast data type.
|
|
||||||
|
|
||||||
Of course, being written in Rust, Bamboo utilizes Rust's type system to define
|
|
||||||
strict API specifications. Invariants, optional or mandatory fields, and data
|
|
||||||
types are explicitly encoded using `struct`s, `enum`s, and `serde` for
|
|
||||||
serialization and deserialization, removing any ambiguity in the specification.
|
|
||||||
This is important for an application as dynamic as Bamboo, as the vast range of
|
|
||||||
content that it's capable of serving is intentionally vague and thus incorrect
|
|
||||||
assumptions by the server or its clients can make for inconsistent, incorrect,
|
|
||||||
or buggy code.
|
|
||||||
|
|
||||||
The public repository for the project can be found at
|
|
||||||
[git.nickzana.dev/bamboo/bamboo](https://git.nickzana.dev/bamboo/bamboo).
|
|
@ -1,268 +0,0 @@
|
|||||||
+++
|
|
||||||
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.
|
|
@ -1,19 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "iso639_enum"
|
|
||||||
weight = 4
|
|
||||||
[taxonomies]
|
|
||||||
tags = []
|
|
||||||
+++
|
|
||||||
|
|
||||||
`iso639_enum` is a small Rust crate I wrote for my Bamboo media server project.
|
|
||||||
ISO639 is a standard that enumerates world languages and provides two and three
|
|
||||||
character codes to represent them. This is important in the context of a media
|
|
||||||
server because any media in a particular language (basically anything with
|
|
||||||
spoken or written words) will represent its language in its metadata in some
|
|
||||||
form, usually as a two or three character language code.
|
|
||||||
|
|
||||||
The documentation for the crate can be found at
|
|
||||||
[lib.rs/iso639_enum](https://lib.rs/iso639_enum).
|
|
||||||
|
|
||||||
The repository for the crate can be found at
|
|
||||||
[git.nickzana.dev/nick/rust-iso639](https://git.nickzana.dev/nick/rust-iso639).
|
|
Loading…
Reference in New Issue