use std::path::{Path, PathBuf};
use bottlers::{Keychain, PrivateKey};
use purecrypto::der::{pem_decode, pem_encode, Reader};
use purecrypto::ec::boxed::BoxedEcdsaPrivateKey;
use purecrypto::ec::ecdsa::EcdsaPrivateKey;
use purecrypto::ec::CurveId;
use crate::error::{Error, Result};
use crate::identity::clone_private_key;
pub struct DiskStore {
path: PathBuf,
keys: Vec<PrivateKey>,
}
impl DiskStore {
pub fn new() -> Result<DiskStore> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))
.ok_or_else(|| Error::Other("cannot determine config directory".into()))?;
Self::with_path(base.join("spot"))
}
pub fn with_path(path: impl Into<PathBuf>) -> Result<DiskStore> {
let path = path.into();
std::fs::create_dir_all(&path)?;
restrict_dir(&path);
let mut store = DiskStore {
path,
keys: Vec::new(),
};
store.load_keys()?;
if store.keys.is_empty() {
store.generate_key()?;
}
Ok(store)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn keychain(&self) -> Result<Keychain> {
let mut kc = Keychain::new();
for key in &self.keys {
kc.add_key(clone_private_key(key)?)?;
}
Ok(kc)
}
pub fn add_key(&mut self, key: PrivateKey, key_type: &str) -> Result<()> {
let der = private_key_pkcs8(&key)?;
let pem = pem_encode("PRIVATE KEY", &der);
let mut path = self.path.join(format!("id_{key_type}.key"));
let mut n = 0;
while path.exists() {
n += 1;
path = self.path.join(format!("id_{key_type}_{n}.key"));
}
write_restricted(&path, pem.as_bytes())?;
self.keys.push(key);
Ok(())
}
fn load_keys(&mut self) -> Result<()> {
let entries = match std::fs::read_dir(&self.path) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(Error::Io(e)),
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("id_") || !name.ends_with(".key") {
continue;
}
if let Ok(data) = std::fs::read_to_string(entry.path()) {
if let Ok(key) = parse_key_pem(&data) {
self.keys.push(key);
}
}
}
Ok(())
}
fn generate_key(&mut self) -> Result<()> {
let sk = EcdsaPrivateKey::generate(&mut purecrypto::rng::OsRng);
self.add_key(PrivateKey::Ecdsa(sk), "ecdsa")
}
}
fn parse_key_pem(pem: &str) -> Result<PrivateKey> {
let der = pem_decode(pem, "PRIVATE KEY")
.map_err(|e| Error::Other(format!("failed to decode PEM block: {e:?}")))?;
if let Ok(boxed) = BoxedEcdsaPrivateKey::from_pkcs8_der(&der) {
if boxed.curve() != CurveId::P256 {
return Err(Error::Other("only P-256 ECDSA keys are supported".into()));
}
return Ok(PrivateKey::Ecdsa(ecdsa_from_boxed(&boxed)?));
}
if let Ok(rsa) = purecrypto::rsa::BoxedRsaPrivateKey::from_pkcs8_der(&der) {
return Ok(PrivateKey::Rsa(rsa));
}
Err(Error::Other("unsupported private key type".into()))
}
fn private_key_pkcs8(key: &PrivateKey) -> Result<Vec<u8>> {
match key {
PrivateKey::Ecdsa(sk) => {
let boxed = BoxedEcdsaPrivateKey::from_bytes(CurveId::P256, &sk.to_bytes())
.map_err(|e| Error::Other(format!("bad ecdsa key: {e:?}")))?;
Ok(boxed.to_pkcs8_der())
}
PrivateKey::Rsa(sk) => Ok(sk.to_pkcs8_der()),
_ => Err(Error::Other("unsupported key type for disk storage".into())),
}
}
fn ecdsa_from_boxed(boxed: &BoxedEcdsaPrivateKey) -> Result<EcdsaPrivateKey> {
let sec1 = boxed.to_sec1_der();
let parse = || -> std::result::Result<EcdsaPrivateKey, String> {
let mut r = Reader::new(&sec1);
let mut seq = r.read_sequence().map_err(|e| format!("{e:?}"))?;
let _version = seq.read_integer_bytes().map_err(|e| format!("{e:?}"))?;
let d = seq.read_octet_string().map_err(|e| format!("{e:?}"))?;
let d: [u8; 32] = d.try_into().map_err(|_| "bad scalar length".to_string())?;
EcdsaPrivateKey::from_bytes(&d).map_err(|e| format!("{e:?}"))
};
parse().map_err(|e| Error::Other(format!("failed to extract ecdsa scalar: {e}")))
}
#[cfg(unix)]
fn restrict_dir(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
}
#[cfg(not(unix))]
fn restrict_dir(_path: &Path) {}
fn write_restricted(path: &Path, data: &[u8]) -> Result<()> {
std::fs::write(path, data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_and_reload() {
let dir = std::env::temp_dir().join(format!("spotlib-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let store = DiskStore::with_path(&dir).unwrap();
let kc = store.keychain().unwrap();
let pkix1 = kc.first_signer().unwrap().public_pkix().unwrap();
let store2 = DiskStore::with_path(&dir).unwrap();
let kc2 = store2.keychain().unwrap();
let pkix2 = kc2.first_signer().unwrap().public_pkix().unwrap();
assert_eq!(pkix1, pkix2);
let _ = std::fs::remove_dir_all(&dir);
}
}