signatory/key/store/
fs.rsuse crate::{Error, KeyHandle, KeyInfo, KeyName, KeyRing, LoadPkcs8, Result};
use pkcs8::der::pem::PemLabel;
use std::{
fs,
path::{Path, PathBuf},
};
use zeroize::Zeroizing;
#[cfg(unix)]
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
#[cfg(unix)]
const REQUIRED_DIR_MODE: u32 = 0o700;
const PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN PRIVATE KEY-----";
const ENCRYPTED_PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
pub struct FsKeyStore {
path: PathBuf,
}
impl FsKeyStore {
pub fn create_or_open(dir_path: &Path) -> Result<Self> {
Self::open(dir_path).or_else(|_| Self::create(dir_path))
}
pub fn create(dir_path: &Path) -> Result<Self> {
fs::create_dir_all(dir_path)?;
#[cfg(unix)]
fs::set_permissions(dir_path, Permissions::from_mode(REQUIRED_DIR_MODE))?;
Self::open(dir_path)
}
pub fn open(dir_path: &Path) -> Result<Self> {
let path = dir_path.canonicalize()?;
let st = path.metadata()?;
if !st.is_dir() {
return Err(Error::NotADirectory);
}
#[cfg(unix)]
if st.permissions().mode() & 0o777 != REQUIRED_DIR_MODE {
return Err(Error::Permissions);
}
Ok(Self { path })
}
pub fn info(&self, name: &KeyName) -> Result<KeyInfo> {
let pem_data = Zeroizing::new(fs::read_to_string(self.key_path(name))?);
let encrypted = if pem_data.starts_with(ENCRYPTED_PRIVATE_KEY_BOUNDARY) {
true
} else if pem_data.starts_with(PRIVATE_KEY_BOUNDARY) {
false
} else {
return Err(pkcs8::Error::KeyMalformed.into());
};
let algorithm = if encrypted {
None
} else {
let (label, der) = pkcs8::SecretDocument::from_pem(&pem_data)?;
pkcs8::PrivateKeyInfo::validate_pem_label(label)?;
der.decode_msg::<pkcs8::PrivateKeyInfo<'_>>()?
.algorithm
.try_into()
.ok()
};
Ok(KeyInfo {
name: name.clone(),
algorithm,
encrypted,
})
}
pub fn import(&self, name: &KeyName, key_ring: &mut KeyRing) -> Result<KeyHandle> {
key_ring.load_pkcs8(self.load(name)?.decode_msg()?)
}
pub fn load(&self, name: &KeyName) -> Result<pkcs8::SecretDocument> {
let (label, doc) = pkcs8::SecretDocument::read_pem_file(self.key_path(name))?;
pkcs8::PrivateKeyInfo::validate_pem_label(&label)?;
Ok(doc)
}
pub fn store(&self, name: &KeyName, der: &pkcs8::SecretDocument) -> Result<()> {
der.write_pem_file(
self.key_path(name),
pkcs8::PrivateKeyInfo::PEM_LABEL,
Default::default(),
)?;
Ok(())
}
pub fn delete(&self, name: &KeyName) -> Result<()> {
fs::remove_file(self.key_path(name))?;
Ok(())
}
fn key_path(&self, name: &KeyName) -> PathBuf {
let mut path = self.path.join(name);
path.set_extension("pem");
path
}
}
#[cfg(test)]
#[allow(unused_imports)] mod tests {
use super::FsKeyStore;
use crate::{Algorithm, GeneratePkcs8};
#[cfg(feature = "secp256k1")]
use crate::ecdsa::secp256k1;
pub const EXAMPLE_KEY: &str = "example-key";
pub struct FsStoreHandle {
pub keystore: FsKeyStore,
pub dir: tempfile::TempDir,
}
#[allow(dead_code)]
fn create_example_keystore(example_key: &pkcs8::SecretDocument) -> FsStoreHandle {
let dir = tempfile::tempdir().unwrap();
let keystore = FsKeyStore::create_or_open(&dir.path().join("keys")).unwrap();
keystore
.store(&EXAMPLE_KEY.parse().unwrap(), example_key)
.unwrap();
FsStoreHandle { keystore, dir }
}
#[cfg(feature = "secp256k1")]
#[test]
fn import_and_delete_key() {
let key_name = EXAMPLE_KEY.parse().unwrap();
let example_key = secp256k1::SigningKey::generate_pkcs8();
let ks = create_example_keystore(&example_key);
let example_key2 = ks.keystore.load(&key_name).unwrap();
assert_eq!(example_key.as_bytes(), example_key2.as_bytes());
ks.keystore.delete(&key_name).unwrap();
}
#[cfg(feature = "secp256k1")]
#[test]
fn get_key_info() {
let key_name = EXAMPLE_KEY.parse().unwrap();
let example_key = secp256k1::SigningKey::generate_pkcs8();
let ks = create_example_keystore(&example_key);
let key_info = ks.keystore.info(&key_name).unwrap();
assert_eq!(key_info.name, key_name);
assert_eq!(key_info.algorithm, Some(Algorithm::EcdsaSecp256k1));
assert!(!key_info.encrypted);
}
}