use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine as _;
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
pub fn file_id(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
pub const HEADER: &str = "gunnar-crypt keyring v1";
pub const KEY_LEN: usize = 64;
pub const ID_KEY_LEN: usize = 32;
#[derive(Clone, PartialEq, Eq)]
pub struct IdKey(Zeroizing<[u8; ID_KEY_LEN]>);
impl IdKey {
pub fn from_bytes(bytes: [u8; ID_KEY_LEN]) -> IdKey {
IdKey(Zeroizing::new(bytes))
}
#[cfg(feature = "keygen")]
pub fn generate() -> IdKey {
let mut bytes = Zeroizing::new([0u8; ID_KEY_LEN]);
getrandom::getrandom(&mut bytes[..]).expect("the OS random source is available");
IdKey(bytes)
}
pub fn bytes(&self) -> &[u8; ID_KEY_LEN] {
&self.0
}
}
impl std::fmt::Debug for IdKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "IdKey(…)")
}
}
#[derive(Clone)]
pub struct DataKey {
epoch: u32,
bytes: Zeroizing<[u8; KEY_LEN]>,
}
impl DataKey {
pub fn from_bytes(epoch: u32, bytes: [u8; KEY_LEN]) -> DataKey {
DataKey {
epoch,
bytes: Zeroizing::new(bytes),
}
}
#[cfg(feature = "keygen")]
pub fn generate(epoch: u32) -> DataKey {
let mut bytes = Zeroizing::new([0u8; KEY_LEN]);
getrandom::getrandom(&mut bytes[..]).expect("the OS random source is available");
DataKey { epoch, bytes }
}
pub fn epoch(&self) -> u32 {
self.epoch
}
pub fn bytes(&self) -> &[u8; KEY_LEN] {
&self.bytes
}
pub fn id(&self) -> String {
let mut h = Sha256::new();
h.update(b"gunnar-crypt key id v1\0");
h.update(&self.bytes[..]);
let d = h.finalize();
d[..8].iter().map(|b| format!("{b:02x}")).collect()
}
}
impl std::fmt::Debug for DataKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DataKey {{ epoch: {}, id: {} }}", self.epoch, self.id())
}
}
impl PartialEq for DataKey {
fn eq(&self, other: &Self) -> bool {
self.epoch == other.epoch && self.bytes[..] == other.bytes[..]
}
}
impl Eq for DataKey {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyringError {
NotAKeyring,
BadLine(usize),
BadEpochs,
Empty,
BadIdKey,
}
impl std::fmt::Display for KeyringError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyringError::NotAKeyring => write!(f, "not a gunnar-crypt key ring"),
KeyringError::BadLine(n) => write!(f, "key ring line {n} is malformed"),
KeyringError::BadEpochs => {
write!(f, "key ring epochs must run 1, 2, 3, … without gaps")
}
KeyringError::Empty => write!(f, "the key ring holds no key"),
KeyringError::BadIdKey => {
write!(f, "the key ring's id-key line is missing or malformed")
}
}
}
}
impl std::error::Error for KeyringError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContinuityError {
Dropped(u32),
Replaced(u32),
IdKeyChanged,
}
impl std::fmt::Display for ContinuityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContinuityError::Dropped(e) => write!(
f,
"the new key ring drops epoch {e}, which this clone holds — refused \
(a key ring only ever grows; this one may have been substituted)"
),
ContinuityError::Replaced(e) => write!(
f,
"the new key ring has a different key for epoch {e} than this clone \
holds — refused (a substituted key ring would let its maker read \
everything written after it)"
),
ContinuityError::IdKeyChanged => write!(
f,
"the new key ring has a different id key — refused (the id key is made \
once and never changes; this ring may have been substituted)"
),
}
}
}
impl std::error::Error for ContinuityError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Keyring {
id_key: IdKey,
keys: Vec<DataKey>,
}
impl Keyring {
pub fn new(id_key: IdKey, first: DataKey) -> Keyring {
assert_eq!(first.epoch, 1, "a key ring starts at epoch 1");
Keyring {
id_key,
keys: vec![first],
}
}
#[cfg(feature = "keygen")]
pub fn generate() -> Keyring {
Keyring::new(IdKey::generate(), DataKey::generate(1))
}
pub fn id_key(&self) -> &IdKey {
&self.id_key
}
pub fn truncated(&self, epoch: u32) -> Keyring {
Keyring {
id_key: self.id_key.clone(),
keys: self.keys[..(epoch as usize).min(self.keys.len()).max(1)].to_vec(),
}
}
#[cfg(feature = "keygen")]
pub fn rotate(&mut self) -> u32 {
let next = self.newest().epoch + 1;
self.keys.push(DataKey::generate(next));
next
}
pub fn get(&self, epoch: u32) -> Option<&DataKey> {
if epoch == 0 {
return None;
}
self.keys.get(epoch as usize - 1)
}
pub fn newest(&self) -> &DataKey {
self.keys.last().expect("a key ring is never empty")
}
pub fn keys(&self) -> &[DataKey] {
&self.keys
}
pub fn to_text(&self) -> Zeroizing<String> {
let mut s = String::with_capacity(32 + self.keys.len() * 100);
s.push_str(HEADER);
s.push('\n');
s.push_str(&format!("id-key {}\n", B64.encode(&self.id_key.0[..])));
for k in &self.keys {
s.push_str(&format!("epoch {} {}\n", k.epoch, B64.encode(&k.bytes[..])));
}
Zeroizing::new(s)
}
pub fn parse(text: &str) -> Result<Keyring, KeyringError> {
let mut lines = text.lines();
if lines.next().map(str::trim_end) != Some(HEADER) {
return Err(KeyringError::NotAKeyring);
}
let mut keys = Vec::new();
let mut id_key = None;
for (i, line) in lines.enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(b) = line.strip_prefix("id-key ") {
if id_key.is_some() {
return Err(KeyringError::BadIdKey);
}
let raw = Zeroizing::new(B64.decode(b.trim()).map_err(|_| KeyringError::BadIdKey)?);
let bytes: [u8; ID_KEY_LEN] =
raw[..].try_into().map_err(|_| KeyringError::BadIdKey)?;
id_key = Some(IdKey::from_bytes(bytes));
continue;
}
let mut parts = line.split_ascii_whitespace();
let (Some("epoch"), Some(n), Some(b), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(KeyringError::BadLine(i + 2));
};
let epoch: u32 = n.parse().map_err(|_| KeyringError::BadLine(i + 2))?;
let raw = Zeroizing::new(B64.decode(b).map_err(|_| KeyringError::BadLine(i + 2))?);
let bytes: [u8; KEY_LEN] = raw[..]
.try_into()
.map_err(|_| KeyringError::BadLine(i + 2))?;
if epoch as usize != keys.len() + 1 {
return Err(KeyringError::BadEpochs);
}
keys.push(DataKey::from_bytes(epoch, bytes));
}
if keys.is_empty() {
return Err(KeyringError::Empty);
}
let id_key = id_key.ok_or(KeyringError::BadIdKey)?;
Ok(Keyring { id_key, keys })
}
pub fn check_continuity(&self, newer: &Keyring) -> Result<(), ContinuityError> {
if newer.id_key != self.id_key {
return Err(ContinuityError::IdKeyChanged);
}
for k in &self.keys {
match newer.get(k.epoch) {
None => return Err(ContinuityError::Dropped(k.epoch)),
Some(n) if n != k => return Err(ContinuityError::Replaced(k.epoch)),
Some(_) => {}
}
}
Ok(())
}
}