mod crypto;
mod error;
mod format;
mod storage;
mod store;
pub use crate::crypto::{KdfParams, algorithm::Algorithm};
use crate::format::{Header, KeystoreFile, parse, serialize};
pub use crate::storage::Storage;
use crate::store::SecretEntry;
use anyhow::{Context, Result, bail};
use directories::ProjectDirs;
use serde::Serialize;
use std::path::PathBuf;
use store::Store;
use zeroize::{Zeroize, Zeroizing};
pub struct Keynest {
store: Store,
storage: Storage,
key: [u8; 32],
keystore_file: KeystoreFile,
}
impl Drop for Keynest {
fn drop(&mut self) {
self.key.zeroize();
}
}
impl Keynest {
pub fn init(password: Zeroizing<String>) -> Result<Self> {
Self::init_with_kdf(password, KdfParams::default())
}
pub fn init_with_kdf(password: Zeroizing<String>, kdf: KdfParams) -> Result<Self> {
let storage = default_storage()?;
Self::init_with_storage_and_kdf(password, storage, kdf)
}
pub fn init_with_storage_and_kdf(
password: Zeroizing<String>,
storage: Storage,
kdf: KdfParams,
) -> Result<Self> {
if storage.exists() {
bail!(
"keystore already exists: {}\nUse `keynest rekey` or remove the file.",
storage.path().display()
);
}
let store = Store::new();
let salt = crypto::generate_salt()?;
let key =
crypto::derive_key(&password, &salt, kdf).context("failed to derive encryption key")?;
drop(password);
let plaintext = Zeroizing::new(serde_json::to_vec(&store)?);
let (header, ciphertext) = Header::encrypt_store(
kdf,
Algorithm::XChaCha20Poly1305,
salt.to_vec(),
&key,
&plaintext,
)?;
let keystore_file = KeystoreFile::new(header, ciphertext);
let file = serialize(&keystore_file)?;
storage.save(&file)?;
Ok(Self {
store,
storage,
key,
keystore_file,
})
}
pub fn open(password: Zeroizing<String>) -> Result<Self> {
let storage = default_storage()?;
Self::open_with_storage(password, storage)
}
pub fn open_with_storage(password: Zeroizing<String>, storage: Storage) -> Result<Self> {
if !storage.exists() {
bail!(
"keystore does not exist: {}\nRun `keynest init` first.",
storage.path().display()
);
}
let data = storage.load()?;
let keystore_file = parse(&data)?;
let key = crypto::derive_key(&password, keystore_file.salt(), *keystore_file.kdf())
.context("unable to derive encryption key")?;
drop(password);
let plaintext = keystore_file.decrypt(&key)?;
let store = serde_json::from_slice(&plaintext)
.context("failed to deserialize keystore; possibly wrong password or corrupted data")?;
Ok(Self {
store,
storage,
key,
keystore_file,
})
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
self.store.set(key, value)?;
Ok(())
}
pub fn get(&self, key: &str) -> Option<&str> {
self.store.get(key)
}
pub fn update(&mut self, key: &str, value: &str) -> Result<()> {
self.store.update(key, value)?;
Ok(())
}
pub fn remove(&mut self, key: &str) -> Result<()> {
self.store.remove(key)?;
Ok(())
}
pub fn list(&self) -> Vec<&String> {
self.store.keys().collect()
}
pub fn list_all(&self) -> Vec<&SecretEntry> {
self.store.entries().collect()
}
pub fn save(&mut self) -> Result<()> {
let plaintext = Zeroizing::new(serde_json::to_vec(&self.store)?);
let (header, ciphertext) = Header::encrypt_store(
*self.keystore_file.kdf(),
self.keystore_file.algorithm(),
self.keystore_file.salt().to_vec(),
&self.key,
&plaintext,
)?;
self.keystore_file = KeystoreFile::new(header, ciphertext);
let file = serialize(&self.keystore_file)?;
self.storage.save(&file)?;
Ok(())
}
pub fn info(&self) -> Result<StoreInfo> {
let metadata = std::fs::metadata(self.storage.path())?;
Ok(StoreInfo {
path: self.storage.path().to_path_buf(),
file_size: metadata.len(),
creation_date: self.store.creation_date().to_string(),
secrets_count: self.store.len(),
kdf: *self.keystore_file.kdf(),
algorithm: self.keystore_file.algorithm().name(),
nonce_len: self.keystore_file.nonce().len(),
version: self.keystore_file.version(),
})
}
pub fn rekey(&mut self, new_password: Zeroizing<String>, new_kdf: KdfParams) -> Result<()> {
let current_algorithm = self.keystore_file.algorithm();
self.rekey_with_algorithm(new_password, new_kdf, current_algorithm)
}
fn rekey_with_algorithm(
&mut self,
new_password: Zeroizing<String>,
new_kdf: KdfParams,
new_algorithm: Algorithm,
) -> Result<()> {
let new_salt = crypto::generate_salt()?;
let new_key = crypto::derive_key(&new_password, &new_salt, new_kdf)
.context("failed to derive new encryption key")?;
drop(new_password);
let plaintext = Zeroizing::new(serde_json::to_vec(&self.store)?);
let (header, ciphertext) = Header::encrypt_store(
new_kdf,
new_algorithm,
new_salt.to_vec(),
&new_key,
&plaintext,
)?;
self.keystore_file = KeystoreFile::new(header, ciphertext);
let file = serialize(&self.keystore_file)?;
self.storage.save(&file)?;
self.key.zeroize();
self.key = new_key;
Ok(())
}
}
pub fn default_storage() -> Result<Storage> {
let project_dirs =
ProjectDirs::from("", "", "keynest").context("could not determine platform directories")?;
let path = project_dirs.data_dir().join(".keynest.db");
Ok(Storage::new(path))
}
#[derive(Serialize)]
pub struct StoreInfo {
path: PathBuf,
file_size: u64,
creation_date: String,
secrets_count: usize,
kdf: KdfParams,
algorithm: &'static str,
nonce_len: usize,
version: u8,
}
impl StoreInfo {
pub fn creation_date(&self) -> &str {
&self.creation_date
}
pub fn secrets_count(&self) -> usize {
self.secrets_count
}
pub fn kdf(&self) -> &KdfParams {
&self.kdf
}
pub fn algorithm(&self) -> &'static str {
self.algorithm
}
pub fn nonce_len(&self) -> usize {
self.nonce_len
}
pub fn version(&self) -> u8 {
self.version
}
pub fn file_size(&self) -> u64 {
self.file_size
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
impl std::fmt::Display for StoreInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Keynest Store Information")?;
writeln!(f, "────────────────────────────────────────")?;
writeln!(f)?;
writeln!(f, "Location")?;
writeln!(f, " Path: {}", self.path.display())?;
writeln!(f, " Size: {}", format_size(self.file_size))?;
writeln!(f, " Format version: {}", self.version)?;
writeln!(f)?;
writeln!(f, "Metadata")?;
writeln!(f, " Created: {}", self.creation_date)?;
writeln!(f, " Secrets stored: {}", self.secrets_count)?;
writeln!(f)?;
writeln!(f, "Encryption")?;
writeln!(f, " Algorithm: {}", self.algorithm)?;
writeln!(f, " Nonce length: {} bytes", self.nonce_len)?;
writeln!(f)?;
writeln!(f, "Key Derivation")?;
writeln!(f, " Memory: {} KiB", self.kdf.mem_cost_kib())?;
writeln!(f, " Time cost: {}", self.kdf.time_cost())?;
writeln!(f, " Parallelism: {}", self.kdf.parallelism())
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn encrypt_decrypt_roundtrip() {
use crate::crypto::*;
use crate::format::{Header, KeystoreFile, parse, serialize};
let kdf = KdfParams::default();
let salt = generate_salt().unwrap();
let key = derive_key("pw", &salt, kdf).unwrap();
let data = b"secret data".to_vec();
let (header, ciphertext) = Header::encrypt_store(
kdf,
Algorithm::XChaCha20Poly1305,
salt.to_vec(),
&key,
&data,
)
.unwrap();
let keystore_file = KeystoreFile::new(header, ciphertext);
let file = serialize(&keystore_file).unwrap();
let keystore_file2 = parse(&file).unwrap();
let key2 = derive_key("pw", keystore_file2.salt(), *keystore_file2.kdf()).unwrap();
let plaintext = keystore_file2.decrypt(&key2).unwrap();
assert_eq!(*plaintext, data);
}
#[test]
fn init_and_open_with_zeroize_wrappers() {
let dir = tempdir().unwrap();
let path = dir.path().join("keynest.db");
let storage = Storage::new(path);
let password = Zeroizing::new(String::from("pw"));
let mut kn =
Keynest::init_with_storage_and_kdf(password, storage.clone(), KdfParams::default())
.unwrap();
kn.set("A", "B").unwrap();
kn.save().unwrap();
let password = Zeroizing::new(String::from("pw"));
let kn2 = Keynest::open_with_storage(password, storage).unwrap();
assert_eq!(kn2.get("A"), Some("B"));
}
#[test]
fn init_fails_if_store_exists() {
let dir = tempdir().unwrap();
let path = dir.path().join("keynest.db");
let storage = Storage::new(path);
let password = Zeroizing::new(String::from("pw"));
Keynest::init_with_storage_and_kdf(password, storage.clone(), KdfParams::default())
.unwrap();
let password = Zeroizing::new(String::from("pw"));
assert!(
Keynest::init_with_storage_and_kdf(password, storage, KdfParams::default()).is_err()
);
}
#[test]
fn wrong_password_fails() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
Keynest::init_with_storage_and_kdf(
Zeroizing::new("correct".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
assert!(Keynest::open_with_storage(Zeroizing::new("wrong".to_string()), storage).is_err());
}
#[test]
fn set_existing_key_fails() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
assert!(kn.set("A", "C").is_err());
}
#[test]
fn update_key_works() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
kn.update("A", "C").unwrap();
assert_eq!(kn.get("A").unwrap(), "C");
}
#[test]
fn update_not_existing_key_fails() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
assert!(kn.update("Z", "C").is_err());
}
#[test]
fn removing_key_works() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
assert_eq!(kn.get("A").unwrap(), "B");
kn.remove("A").unwrap();
assert_eq!(kn.get("A"), None);
}
#[test]
fn removing_not_existing_key_fails() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
assert!(kn.remove("A").is_err());
}
#[test]
fn list_works() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
assert!(kn.list().contains(&&"A".to_string()));
assert!(!kn.list().contains(&&"B".to_string()));
}
#[test]
fn list_all_works() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
for sec_entry in kn.list_all() {
assert_eq!(sec_entry.key(), "A");
assert_eq!(sec_entry.value(), "B");
assert_ne!(sec_entry.updated(), "");
}
}
#[test]
fn rekey_changes_password() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("old".to_string()),
storage.clone(),
KdfParams::default(),
)
.unwrap();
kn.set("A", "B").unwrap();
kn.save().unwrap();
let mut kn =
Keynest::open_with_storage(Zeroizing::new("old".to_string()), storage.clone()).unwrap();
kn.rekey(Zeroizing::new("new".to_string()), KdfParams::default())
.unwrap();
assert!(
Keynest::open_with_storage(Zeroizing::new("old".to_string()), storage.clone()).is_err()
);
let kn2 = Keynest::open_with_storage(Zeroizing::new("new".to_string()), storage).unwrap();
assert_eq!(kn2.get("A"), Some("B"));
}
#[test]
fn rekey_changes_kdf_parameters() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path().join("keynest.db"));
let original_kdf = KdfParams::default();
let mut kn = Keynest::init_with_storage_and_kdf(
Zeroizing::new("pw".to_string()),
storage.clone(),
original_kdf,
)
.unwrap();
kn.save().unwrap();
let mut kn =
Keynest::open_with_storage(Zeroizing::new("pw".to_string()), storage.clone()).unwrap();
let new_kdf = KdfParams::new(
original_kdf.mem_cost_kib() * 2,
original_kdf.time_cost() + 1,
original_kdf.parallelism(),
)
.unwrap();
kn.rekey(Zeroizing::new("pw".to_string()), new_kdf).unwrap();
let kn2 = Keynest::open_with_storage(Zeroizing::new("pw".to_string()), storage).unwrap();
assert_eq!(
kn2.keystore_file.kdf().mem_cost_kib(),
new_kdf.mem_cost_kib()
);
assert_eq!(kn2.keystore_file.kdf().time_cost(), new_kdf.time_cost());
}
}