Skip to content

Commit

Permalink
zeroizing, write README
Browse files Browse the repository at this point in the history
  • Loading branch information
GoldsteinE committed Sep 3, 2022
1 parent 731481f commit c1db31e
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ serde = { version = "1.0.144", features = ["derive"] }
serde_cbor = "0.11.2"
serde_json = "1.0.85"
toml = "0.5.9"
zeroize = "1.5.7"

[dev-dependencies]
proptest = "1.0.0"
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# classified

A simpler NixOS secrets management system.

## Problem

There’re some secrets management systems for NixOS ([agenix], [sops-nix]), but they require kinda complicated setup. In many cases it’s enough to just decrypt all the secrets with root-readable key file present on disk (if someone has root access to your computer, you’re probably already royally fucked).

## Solution

Use a simple encryption program and put the config into a NixOS module. Just like this:

```nix
# flake.nix
{
inputs = {
classified = {
url = "github:GoldsteinE/classified";
# to avoid having one more copy of nixpkgs
inputs.nixpkgs.follows = "nixpkgs";
# you can also do this with rust-overlay and naersk
};
};
outputs = { nixpkgs, classified, ... }: {
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem rec {
system = "x86_64-linux";
modules = [
# other modules here
./configuration.nix
classified.nixosModules."${system}".default
];
};
};
}
```

and then

```nix
# configuration.nix
{
classified = {
# Default is `/var/secrets`
targetDir = "/var/classfied";
keys = {
first = "/path/to/first.key";
second = "/path/to/second.key";
};
files = {
top-secret = {
# You can omit the `key` attribute if you have exactly one key configured
key = "first";
encrypted = ./encrypted-file;
# Default is `400`
mode = "440";
# Defaults are `root:root`
user = "nginx";
group = "nogroup";
};
};
};
}
```

### Generating keys and encrypting data

```shell
# (as root)
umask 377 # so the key file has the right permissions
classified gen-key > /path/to/key
cat /path/to/key # key is just 24 words, so you can write it down
umask 022 # it's ok for encrypted data to be world-readable
classified encrypt --key /path/to/key /path/to/secret-data > /path/to/encrypted-data
# if you ever want to manually decrypt it
classified decrypt --key /path/to/key /path/to/encrypted-data
```

### What’s inside?

* `XChaCha20-Poly1305` which is proven secure. The nonce is chosen randomly for every encrypted file.

* A fresh `tmpfs` is created on every decryption, so old secrets are not available.

* No temporary files are written, no Rust unsafe code is used, and the codebase is small and easy to audit yourself.

* It attempts to zeroize any keys and decrypted files before deallocating memory.

### So it’s secure?

I mean...

* It didn’t pass any kind of professional security review.

* Zeroizing memory is hard and should be considered best-effort.

So (as with any cryptography project) — use at your own risk.

[agenix]: https://github.com/ryantm/agenix
[sops-nix]: https://github.com/Mic92/sops-nix
9 changes: 8 additions & 1 deletion src/keyarmor/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bitvec::{array::BitArray, order::Msb0};
use color_eyre::eyre::{self, eyre, ensure};
use color_eyre::eyre::{self, ensure, eyre};
use crc_any::CRCu8;
use zeroize::Zeroize as _;

pub struct Words {
inner: BitArray<[u8; 33], Msb0>,
Expand Down Expand Up @@ -57,6 +58,12 @@ impl Words {
}
}

impl Drop for Words {
fn drop(&mut self) {
self.inner.as_raw_mut_slice().zeroize();
}
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
Expand Down
20 changes: 15 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// lint me harder
#![forbid(non_ascii_idents)]
#![forbid(unsafe_code)]
#![deny(
future_incompatible,
keyword_idents,
Expand All @@ -22,8 +23,7 @@
#![warn(clippy::pedantic)]

use std::{
fmt::{self},
fs,
fmt, fs,
io::{self, Read as _, Write as _},
ops::Deref,
path::{Path, PathBuf},
Expand All @@ -38,6 +38,7 @@ use color_eyre::eyre::{self, eyre, WrapErr as _};
use indexmap::IndexMap;
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize as _;

use crate::config::{Config, FileDesc};

Expand Down Expand Up @@ -150,6 +151,12 @@ impl Deref for ArmoredKey {
}
}

impl Drop for ArmoredKey {
fn drop(&mut self) {
self.inner.zeroize();
}
}

fn main() -> eyre::Result<()> {
color_eyre::install()?;

Expand All @@ -162,7 +169,7 @@ fn main() -> eyre::Result<()> {
Command::Encrypt { key, file } => {
let cipher = Cipher::new(&*ArmoredKey::from_file(&key)?);
let nonce = Cipher::generate_nonce(&mut rng);
let plaintext = maybe_stdin(file.as_deref())?;
let mut plaintext = maybe_stdin(file.as_deref())?;
let bytes = cipher
.encrypt(&nonce, plaintext.as_slice())
.map_err(|_| eyre!("failed to encrypt"))?;
Expand All @@ -171,16 +178,18 @@ fn main() -> eyre::Result<()> {
let mut out = io::stdout().lock();
out.write_all(base64::encode(cbor.as_slice()).as_bytes())?;
out.write_all(b"\n")?;
plaintext.zeroize();
}
Command::Decrypt { key, file } => {
let cipher = Cipher::new(&*ArmoredKey::from_file(&key)?);
let armored = maybe_stdin(file.as_deref())?;
let decrypted = decrypt(
let mut decrypted = decrypt(
file.as_deref().unwrap_or_else(|| "-".as_ref()),
&cipher,
&armored,
)?;
io::stdout().write_all(&decrypted)?;
decrypted.zeroize();
}
Command::Batch { config } => {
let config = Config::parse(&maybe_stdin(config.as_deref())?)?;
Expand Down Expand Up @@ -214,9 +223,10 @@ fn main() -> eyre::Result<()> {
})
.collect::<eyre::Result<_>>()?;

for (file, name, contents) in decrypted {
for (file, name, mut contents) in decrypted {
let path = config.target_dir.join(name);
file.create(&path, &contents)?;
contents.zeroize();
}
}
}
Expand Down

0 comments on commit c1db31e

Please sign in to comment.