use std::fmt;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::Path;
use zeroize::Zeroizing;
use super::{Certificate, KeyError, PrivateKey, SigningIdentity};
use crate::error::{Error, FileOperation};
const JKS_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfe, 0xed];
enum Store {
Jks { store: jks::KeyStore, password: Zeroizing<String> },
Pkcs12 { store: p12_keystore::KeyStore, password: Zeroizing<String> },
}
pub struct Keystore(Store);
impl fmt::Debug for Keystore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let format = match self.0 {
Store::Jks { .. } => "JKS",
Store::Pkcs12 { .. } => "PKCS#12",
};
f.debug_struct("Keystore").field("format", &format).field("aliases", &self.aliases()).finish()
}
}
impl Keystore {
pub fn open(path: impl AsRef<Path>, password: &str) -> Result<Self, Error> {
let path = path.as_ref();
let bytes = Zeroizing::new(std::fs::read(path).map_err(Error::file(FileOperation::Read, path))?);
Ok(Self::from_bytes(&bytes, password)?)
}
pub fn from_bytes(bytes: &[u8], password: &str) -> Result<Self, KeyError> {
let parsed = catch_unwind(AssertUnwindSafe(|| -> Result<Self, KeyError> {
if bytes.starts_with(&JKS_MAGIC) {
let mut store = jks::KeyStore::new();
store.load(bytes, password.as_bytes())?;
Ok(Self(Store::Jks { store, password: Zeroizing::new(password.to_owned()) }))
} else {
let policy = p12_keystore::Pkcs12ImportPolicy::Strict;
let store = p12_keystore::KeyStore::from_pkcs12(bytes, password, policy)?;
Ok(Self(Store::Pkcs12 { store, password: Zeroizing::new(password.to_owned()) }))
}
}));
parsed.map_err(|_| KeyError::MalformedKeystore)?
}
pub fn aliases(&self) -> Vec<String> {
match &self.0 {
Store::Jks { store, .. } => store.aliases(),
Store::Pkcs12 { store, .. } => store.entries().map(|(alias, _)| alias.clone()).collect(),
}
}
pub fn identity(&self, alias: &str, key_password: Option<&str>) -> Result<SigningIdentity, KeyError> {
let unknown = || KeyError::UnknownAlias { alias: alias.to_owned() };
let (key, chain) = match &self.0 {
Store::Jks { store, password } => {
if !store.is_private_key_entry(alias) {
return Err(if store.aliases().iter().any(|a| a == alias) {
KeyError::NotAPrivateKey { alias: alias.to_owned() }
} else {
unknown()
});
}
let password = key_password.unwrap_or(password);
let entry = store.get_private_key_entry(alias, password.as_bytes())?;
let chain: Vec<Vec<u8>> = entry.certificate_chain.into_iter().map(|c| c.content).collect();
(PrivateKey::from_pkcs8_der(&entry.private_key)?, chain)
}
Store::Pkcs12 { store, password } => {
if key_password.is_some_and(|p| p != password.as_str()) {
return Err(KeyError::WrongPassword);
}
match store.entry(alias).ok_or_else(unknown)? {
p12_keystore::KeyStoreEntry::PrivateKeyChain(chain) => (
PrivateKey::from_pkcs8_der(chain.key().as_der())?,
chain.certs().iter().map(|c| c.as_der().to_vec()).collect(),
),
_ => return Err(KeyError::NotAPrivateKey { alias: alias.to_owned() }),
}
}
};
if chain.is_empty() {
return Err(KeyError::MissingCertificate);
}
let chain = chain.iter().map(|der| Certificate::from_der(der)).collect::<Result<_, _>>()?;
SigningIdentity::new(key, chain)
}
pub fn sole_identity(&self, key_password: Option<&str>) -> Result<SigningIdentity, KeyError> {
let aliases: Vec<String> = self.aliases().into_iter().filter(|alias| self.is_private_key(alias)).collect();
match aliases.as_slice() {
[alias] => self.identity(alias, key_password),
_ => Err(KeyError::AmbiguousAlias { count: aliases.len() }),
}
}
fn is_private_key(&self, alias: &str) -> bool {
match &self.0 {
Store::Jks { store, .. } => store.is_private_key_entry(alias),
Store::Pkcs12 { store, .. } => {
matches!(store.entry(alias), Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(_)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const P12: &[u8] = include_bytes!("../../tests/fixtures/keys/test-only.p12");
#[test]
fn unlocks_pkcs12_created_by_keytool() {
let keystore = Keystore::from_bytes(P12, "android-test").unwrap();
assert_eq!(keystore.aliases(), ["postcard"]);
assert!(keystore.identity("postcard", None).is_ok());
assert!(keystore.sole_identity(None).is_ok());
assert!(matches!(keystore.identity("missing", None), Err(KeyError::UnknownAlias { .. })));
assert!(matches!(keystore.identity("postcard", Some("other")), Err(KeyError::WrongPassword)));
assert!(Keystore::from_bytes(P12, "wrong").is_err());
}
#[test]
fn unlocks_jks_with_separate_key_password() {
let identity = SigningIdentity::from_pem(include_str!("../../tests/fixtures/keys/debug.pem")).unwrap();
let key = identity.key().to_pkcs8_der().unwrap();
let mut store = jks::KeyStore::new();
let entry = jks::PrivateKeyEntry {
creation_time: std::time::UNIX_EPOCH,
private_key: key.to_vec(),
certificate_chain: vec![jks::Certificate {
cert_type: "X509".into(),
content: identity.certificate().der().to_vec(),
}],
};
store.set_private_key_entry("release", entry, b"key-pass").unwrap();
let mut bytes = Vec::new();
store.store(&mut bytes, b"store-pass").unwrap();
let keystore = Keystore::from_bytes(&bytes, "store-pass").unwrap();
let loaded = keystore.identity("release", Some("key-pass")).unwrap();
assert_eq!(loaded.certificate(), identity.certificate());
assert!(keystore.identity("release", None).is_err());
}
}