use super::Error;
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM, NONCE_LEN};
use ring::rand::{SecureRandom, SystemRandom};
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WrappedDataKey {
pub version: u32,
pub ciphertext: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyVersions {
pub latest: u32,
pub min_decryption: u32,
}
pub trait KeyProvider: Send + Sync + 'static {
fn name(&self) -> &str;
fn wrap<'a>(&'a self, data_key: &'a [u8; 32]) -> BoxFuture<'a, Result<WrappedDataKey, Error>>;
fn unwrap<'a>(&'a self, wrapped: &'a WrappedDataKey) -> BoxFuture<'a, Result<[u8; 32], Error>>;
fn rewrap<'a>(
&'a self,
wrapped: &'a [WrappedDataKey],
) -> BoxFuture<'a, Result<Vec<Result<WrappedDataKey, Error>>, Error>> {
Box::pin(async move {
let mut out = Vec::with_capacity(wrapped.len());
for one in wrapped {
let rewrapped = match self.unwrap(one).await {
Ok(data_key) => self.wrap(&data_key).await,
Err(err) => Err(err),
};
out.push(rewrapped);
}
Ok(out)
})
}
fn versions(&self) -> BoxFuture<'_, Result<KeyVersions, Error>>;
fn rotate(&self) -> BoxFuture<'_, Result<u32, Error>>;
fn retire_below(&self, version: u32) -> BoxFuture<'_, Result<(), Error>>;
fn key(&self, key: &str) -> Result<std::sync::Arc<dyn KeyProvider>, Error>;
fn create(&self) -> BoxFuture<'_, Result<bool, Error>>;
fn delete(&self) -> BoxFuture<'_, Result<(), Error>>;
}
pub fn validate_key_name(key: &str) -> Result<(), Error> {
let valid = !key.is_empty()
&& key.len() <= 64
&& key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
if valid {
Ok(())
} else {
Err(Error::ProviderRefused {
provider: "skilj".into(),
message: format!("key name {key:?} must be 1 to 64 ASCII letters, digits, '-' or '_'"),
})
}
}
fn random_bytes<const N: usize>() -> [u8; N] {
let mut bytes = [0u8; N];
SystemRandom::new().fill(&mut bytes).expect(
"SystemRandom::fill only fails on catastrophic OS RNG failure - see ring's own docs",
);
bytes
}
#[derive(Clone, Serialize, Deserialize)]
struct Keyring {
versions: Vec<StoredVersion>,
min_decryption_version: u32,
}
#[derive(Clone, Serialize, Deserialize)]
struct StoredVersion {
version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
key: Option<String>,
}
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn from_hex(text: &str) -> Option<[u8; 32]> {
if text.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(&text[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}
impl Keyring {
fn new(first: [u8; 32]) -> Self {
Self {
versions: vec![StoredVersion {
version: 1,
key: Some(to_hex(&first)),
}],
min_decryption_version: 1,
}
}
fn latest(&self) -> u32 {
self.versions.last().map_or(0, |v| v.version)
}
fn key(&self, version: u32) -> Result<[u8; 32], Error> {
if version < self.min_decryption_version {
return Err(Error::KeyVersionRetired { version });
}
let stored = self
.versions
.iter()
.find(|v| v.version == version)
.ok_or(Error::KeyVersionRetired { version })?;
let hex = stored
.key
.as_deref()
.ok_or(Error::KeyVersionRetired { version })?;
from_hex(hex).ok_or_else(|| Error::ProviderRefused {
provider: "local".into(),
message: format!("version {version} is not a 32-byte hex key"),
})
}
fn wrap(&self, data_key: &[u8; 32]) -> Result<WrappedDataKey, Error> {
let version = self.latest();
let key = self.key(version)?;
let nonce = random_bytes::<NONCE_LEN>();
let sealing = LessSafeKey::new(
UnboundKey::new(&AES_256_GCM, &key).expect("a 32-byte key is valid for AES-256-GCM"),
);
let mut sealed = data_key.to_vec();
sealing
.seal_in_place_append_tag(
Nonce::assume_unique_for_key(nonce),
Aad::empty(),
&mut sealed,
)
.expect("sealing a plain byte buffer under a fresh nonce never fails");
let mut ciphertext = nonce.to_vec();
ciphertext.extend_from_slice(&sealed);
Ok(WrappedDataKey {
version,
ciphertext,
})
}
fn unwrap(&self, wrapped: &WrappedDataKey) -> Result<[u8; 32], Error> {
let key = self.key(wrapped.version)?;
if wrapped.ciphertext.len() < NONCE_LEN {
return Err(Error::DecryptFailed);
}
let (nonce, sealed) = wrapped.ciphertext.split_at(NONCE_LEN);
let nonce: [u8; NONCE_LEN] = nonce.try_into().map_err(|_| Error::DecryptFailed)?;
let opening = LessSafeKey::new(
UnboundKey::new(&AES_256_GCM, &key).expect("a 32-byte key is valid for AES-256-GCM"),
);
let mut buffer = sealed.to_vec();
let opened = opening
.open_in_place(
Nonce::assume_unique_for_key(nonce),
Aad::empty(),
&mut buffer,
)
.map_err(|_| Error::DecryptFailed)?;
opened.try_into().map_err(|_| Error::DecryptFailed)
}
fn rotate(&mut self) -> u32 {
let version = self.latest() + 1;
self.versions.push(StoredVersion {
version,
key: Some(to_hex(&random_bytes::<32>())),
});
version
}
fn retire_below(&mut self, version: u32) {
let version = version.min(self.latest());
for stored in &mut self.versions {
if stored.version < version {
stored.key = None;
}
}
self.min_decryption_version = self.min_decryption_version.max(version);
}
fn versions(&self) -> KeyVersions {
KeyVersions {
latest: self.latest(),
min_decryption: self.min_decryption_version,
}
}
}
#[derive(Clone)]
pub struct InMemoryKeyProvider {
keyrings: Arc<Mutex<std::collections::HashMap<String, Keyring>>>,
key: String,
name: String,
}
impl InMemoryKeyProvider {
pub fn new() -> Self {
Self::from_bytes(random_bytes::<32>())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
let mut keyrings = std::collections::HashMap::new();
keyrings.insert(String::new(), Keyring::new(bytes));
Self {
keyrings: Arc::new(Mutex::new(keyrings)),
key: String::new(),
name: "in-memory".into(),
}
}
fn with<T>(&self, f: impl FnOnce(&mut Keyring) -> T) -> Result<T, Error> {
let mut keyrings = self.keyrings.lock().unwrap_or_else(|e| e.into_inner());
let keyring = keyrings
.get_mut(&self.key)
.ok_or_else(|| Error::ProviderRefused {
provider: self.name.clone(),
message: "the key does not exist".into(),
})?;
Ok(f(keyring))
}
}
impl Default for InMemoryKeyProvider {
fn default() -> Self {
Self::new()
}
}
impl KeyProvider for InMemoryKeyProvider {
fn name(&self) -> &str {
&self.name
}
fn wrap<'a>(&'a self, data_key: &'a [u8; 32]) -> BoxFuture<'a, Result<WrappedDataKey, Error>> {
Box::pin(async move { self.with(|k| k.wrap(data_key))? })
}
fn unwrap<'a>(&'a self, wrapped: &'a WrappedDataKey) -> BoxFuture<'a, Result<[u8; 32], Error>> {
Box::pin(async move { self.with(|k| k.unwrap(wrapped))? })
}
fn versions(&self) -> BoxFuture<'_, Result<KeyVersions, Error>> {
Box::pin(async move { self.with(|k| k.versions()) })
}
fn rotate(&self) -> BoxFuture<'_, Result<u32, Error>> {
Box::pin(async move { self.with(Keyring::rotate) })
}
fn retire_below(&self, version: u32) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move { self.with(|k| k.retire_below(version)) })
}
fn key(&self, key: &str) -> Result<Arc<dyn KeyProvider>, Error> {
validate_key_name(key)?;
Ok(Arc::new(Self {
keyrings: Arc::clone(&self.keyrings),
key: key.to_string(),
name: format!("in-memory:{key}"),
}))
}
fn create(&self) -> BoxFuture<'_, Result<bool, Error>> {
Box::pin(async move {
let mut keyrings = self.keyrings.lock().unwrap_or_else(|e| e.into_inner());
if keyrings.contains_key(&self.key) {
return Ok(false);
}
keyrings.insert(self.key.clone(), Keyring::new(random_bytes::<32>()));
Ok(true)
})
}
fn delete(&self) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move {
self.keyrings
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&self.key);
Ok(())
})
}
}
pub struct KeyFileProvider {
path: PathBuf,
lock: Mutex<()>,
name: String,
}
impl KeyFileProvider {
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
let provider = Self {
path: path.as_ref().to_path_buf(),
lock: Mutex::new(()),
name: "file".into(),
};
provider.create_file()?;
provider.read()?;
Ok(provider)
}
fn create_file(&self) -> Result<bool, Error> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
if self.path.exists() {
return Ok(false);
}
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent).map_err(|e| self.io_error(e))?;
}
self.write(&Keyring::new(random_bytes::<32>()))?;
Ok(true)
}
pub fn path(&self) -> &Path {
&self.path
}
fn io_error(&self, err: impl std::fmt::Display) -> Error {
Error::ProviderUnavailable {
provider: self.name.clone(),
message: format!("{}: {err}", self.path.display()),
}
}
fn read(&self) -> Result<Keyring, Error> {
let text = std::fs::read_to_string(&self.path).map_err(|e| self.io_error(e))?;
serde_json::from_str(&text).map_err(|e| Error::ProviderRefused {
provider: self.name.clone(),
message: format!("{}: not a skilj key file: {e}", self.path.display()),
})
}
fn write(&self, keyring: &Keyring) -> Result<(), Error> {
let text = serde_json::to_string_pretty(keyring).expect("a keyring always serialises");
let temporary = self.path.with_extension("tmp");
{
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(&temporary).map_err(|e| self.io_error(e))?;
std::io::Write::write_all(&mut file, text.as_bytes()).map_err(|e| self.io_error(e))?;
file.sync_all().map_err(|e| self.io_error(e))?;
}
std::fs::rename(&temporary, &self.path).map_err(|e| self.io_error(e))
}
fn change<T>(&self, f: impl FnOnce(&mut Keyring) -> T) -> Result<T, Error> {
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let mut keyring = self.read()?;
let out = f(&mut keyring);
self.write(&keyring)?;
Ok(out)
}
}
impl KeyProvider for KeyFileProvider {
fn name(&self) -> &str {
&self.name
}
fn wrap<'a>(&'a self, data_key: &'a [u8; 32]) -> BoxFuture<'a, Result<WrappedDataKey, Error>> {
Box::pin(async move { self.read()?.wrap(data_key) })
}
fn unwrap<'a>(&'a self, wrapped: &'a WrappedDataKey) -> BoxFuture<'a, Result<[u8; 32], Error>> {
Box::pin(async move { self.read()?.unwrap(wrapped) })
}
fn versions(&self) -> BoxFuture<'_, Result<KeyVersions, Error>> {
Box::pin(async move { Ok(self.read()?.versions()) })
}
fn rotate(&self) -> BoxFuture<'_, Result<u32, Error>> {
Box::pin(async move { self.change(Keyring::rotate) })
}
fn retire_below(&self, version: u32) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move { self.change(|k| k.retire_below(version)) })
}
fn key(&self, key: &str) -> Result<Arc<dyn KeyProvider>, Error> {
validate_key_name(key)?;
let stem = self
.path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Ok(Arc::new(Self {
path: self.path.with_file_name(format!("{stem}-{key}.json")),
lock: Mutex::new(()),
name: format!("file:{key}"),
}))
}
fn create(&self) -> BoxFuture<'_, Result<bool, Error>> {
Box::pin(async move { self.create_file() })
}
fn delete(&self) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move {
match std::fs::remove_file(&self.path) {
Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(self.io_error(e)),
_ => Ok(()),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(future)
}
#[test]
fn a_rotated_keyring_still_unwraps_until_the_old_version_is_retired() {
block_on(async {
let provider = InMemoryKeyProvider::new();
let data_key = [9u8; 32];
let old = provider.wrap(&data_key).await.unwrap();
assert_eq!(old.version, 1);
assert_eq!(provider.rotate().await.unwrap(), 2);
assert_eq!(provider.unwrap(&old).await.unwrap(), data_key);
let mut rewrapped = provider.rewrap(std::slice::from_ref(&old)).await.unwrap();
let new = rewrapped.remove(0).unwrap();
assert_eq!(new.version, 2);
provider.retire_below(2).await.unwrap();
assert!(matches!(
provider.unwrap(&old).await,
Err(Error::KeyVersionRetired { version: 1 })
));
assert_eq!(provider.unwrap(&new).await.unwrap(), data_key);
assert_eq!(
provider.versions().await.unwrap(),
KeyVersions {
latest: 2,
min_decryption: 2
}
);
});
}
#[test]
fn a_key_file_keeps_its_versions_and_drops_a_retired_key() {
block_on(async {
let path = std::env::temp_dir().join(format!(
"skilj-key-file-{}.json",
crate::shared::generate_token_id()
));
let provider = KeyFileProvider::open(&path).unwrap();
let data_key = [3u8; 32];
let old = provider.wrap(&data_key).await.unwrap();
provider.rotate().await.unwrap();
let other = KeyFileProvider::open(&path).unwrap();
assert_eq!(other.versions().await.unwrap().latest, 2);
assert_eq!(other.unwrap(&old).await.unwrap(), data_key);
other.retire_below(2).await.unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let keyring: Keyring = serde_json::from_str(&text).unwrap();
assert!(keyring.versions[0].key.is_none(), "the retired key is gone");
assert!(matches!(
provider.unwrap(&old).await,
Err(Error::KeyVersionRetired { version: 1 })
));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
std::fs::remove_file(&path).unwrap();
});
}
}