use std::fmt;
use std::path::Path;
use crate::password_retriever::{Charset, Password};
pub const JKS_MAGIC: u32 = 0xFEED_FEED;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyStoreFormat {
Jks,
Pkcs12,
}
impl KeyStoreFormat {
pub fn from_name(name: &str) -> Result<Self, KeyStoreError> {
let normalized = name.to_ascii_lowercase().replace(['#', '-', '_', ' '], "");
match normalized.as_str() {
"jks" => Ok(KeyStoreFormat::Jks),
"pkcs12" | "p12" | "pfx" => Ok(KeyStoreFormat::Pkcs12),
_ => Err(KeyStoreError::UnsupportedType(format!(
"{} not found: this port provides only the JKS and PKCS12 KeyStore types",
name
))),
}
}
pub fn detect(data: &[u8]) -> Option<Self> {
if data.len() >= 4 && u32::from_be_bytes([data[0], data[1], data[2], data[3]]) == JKS_MAGIC
{
return Some(KeyStoreFormat::Jks);
}
if data.first() == Some(&0x30) {
return Some(KeyStoreFormat::Pkcs12);
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyStoreError {
WrongKeystorePassword(String),
UnrecoverableKey(String),
NoKeyEntry(String),
NotAKeyEntry(String),
NotAPrivateKey {
alias: String,
algorithm: String,
},
Malformed(String),
UnsupportedType(String),
}
impl fmt::Display for KeyStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyStoreError::WrongKeystorePassword(m) => write!(f, "{}", m),
KeyStoreError::UnrecoverableKey(m) => write!(f, "{}", m),
KeyStoreError::NoKeyEntry(alias) | KeyStoreError::NotAKeyEntry(alias) => {
write!(f, "entry \"{}\" does not contain a key", alias)
}
KeyStoreError::NotAPrivateKey { alias, algorithm } => {
write!(
f,
"entry \"{}\" does not contain a private key. It contains a key of \
algorithm: {}",
alias, algorithm
)
}
KeyStoreError::Malformed(m) => write!(f, "{}", m),
KeyStoreError::UnsupportedType(m) => write!(f, "{}", m),
}
}
}
impl std::error::Error for KeyStoreError {}
pub enum KeyStore {
Jks(Box<jks::KeyStore>),
Pkcs12(Box<p12_keystore::KeyStore>),
}
impl fmt::Debug for KeyStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyStore::Jks(_) => f.write_str("KeyStore::Jks(<redacted>)"),
KeyStore::Pkcs12(_) => f.write_str("KeyStore::Pkcs12(<redacted>)"),
}
}
}
impl KeyStore {
pub fn load_from_file(
path: &str,
passwords: &[Password],
format_hint: Option<&str>,
) -> Result<Self, KeyStoreError> {
let hinted_format = match format_hint {
Some(name) => Some(KeyStoreFormat::from_name(name)?),
None => None,
};
let data = std::fs::read(Path::new(path))
.map_err(|e| KeyStoreError::Malformed(format!("Failed to read {}: {}", path, e)))?;
let format = match hinted_format {
Some(format) => format,
None => KeyStoreFormat::detect(&data).ok_or_else(|| {
KeyStoreError::Malformed(format!(
"Failed to determine the KeyStore type of {}: it is neither a JKS \
(0xFEEDFEED magic) nor a PKCS #12 (DER SEQUENCE) file. Specify --ks-type.",
path
))
})?,
};
Self::load(&data, passwords, format)
}
pub fn load(
data: &[u8],
passwords: &[Password],
format: KeyStoreFormat,
) -> Result<Self, KeyStoreError> {
if passwords.is_empty() {
return Err(KeyStoreError::Malformed(
"No keystore passwords".to_string(),
));
}
let mut last_failure: Option<KeyStoreError> = None;
for password in passwords {
match Self::load_once(data, password, format) {
Ok(store) => return Ok(store),
Err(e) => last_failure = Some(e),
}
}
Err(last_failure.expect("at least one password was tried"))
}
fn load_once(
data: &[u8],
password: &[char],
format: KeyStoreFormat,
) -> Result<Self, KeyStoreError> {
let password_bytes = Charset::Utf8.encode(password);
match format {
KeyStoreFormat::Jks => {
let mut store = jks::KeyStore::new();
store.load(data, &password_bytes).map_err(|e| {
KeyStoreError::WrongKeystorePassword(format!(
"Failed to load the KeyStore: {}",
e
))
})?;
Ok(KeyStore::Jks(Box::new(store)))
}
KeyStoreFormat::Pkcs12 => {
let password_str = String::from_iter(password.iter().copied());
let store = p12_keystore::KeyStore::from_pkcs12(
data,
&password_str,
p12_keystore::Pkcs12ImportPolicy::Strict,
)
.map_err(|e| {
KeyStoreError::WrongKeystorePassword(format!(
"Failed to load the KeyStore: {}",
e
))
})?;
Ok(KeyStore::Pkcs12(Box::new(store)))
}
}
}
pub fn aliases(&self) -> Vec<String> {
match self {
KeyStore::Jks(store) => store.aliases(),
KeyStore::Pkcs12(store) => store.entries().map(|(alias, _)| alias.clone()).collect(),
}
}
pub fn is_key_entry(&self, alias: &str) -> bool {
match self {
KeyStore::Jks(store) => store.is_private_key_entry(alias),
KeyStore::Pkcs12(store) => matches!(
store.entry(alias),
Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(_))
),
}
}
pub fn get_key(&self, alias: &str, passwords: &[Password]) -> Result<Vec<u8>, KeyStoreError> {
match self {
KeyStore::Jks(store) => {
if !store.is_private_key_entry(alias) {
return Err(if store.aliases().iter().any(|a| a == alias) {
KeyStoreError::NotAKeyEntry(alias.to_string())
} else {
KeyStoreError::NoKeyEntry(alias.to_string())
});
}
if passwords.is_empty() {
return Err(KeyStoreError::Malformed("No key passwords".to_string()));
}
let mut last_failure: Option<KeyStoreError> = None;
for password in passwords {
let password_bytes = Charset::Utf8.encode(password);
match store.get_private_key_entry(alias, &password_bytes) {
Ok(entry) => return Ok(entry.private_key),
Err(jks::KeyStoreError::EntryNotFound) => {
last_failure = Some(KeyStoreError::NoKeyEntry(alias.to_string()))
}
Err(jks::KeyStoreError::WrongEntryType) => {
last_failure = Some(KeyStoreError::NotAKeyEntry(alias.to_string()))
}
Err(e) => {
last_failure = Some(KeyStoreError::UnrecoverableKey(format!(
"Failed to obtain key with alias \"{}\": {}",
alias, e
)))
}
}
}
Err(last_failure.expect("at least one password was tried"))
}
KeyStore::Pkcs12(store) => match store.entry(alias) {
Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(chain)) => {
Ok(chain.key().as_der().to_vec())
}
Some(p12_keystore::KeyStoreEntry::Certificate(_)) => {
Err(KeyStoreError::NotAKeyEntry(alias.to_string()))
}
Some(p12_keystore::KeyStoreEntry::Secret(secret)) => {
Err(KeyStoreError::NotAPrivateKey {
alias: alias.to_string(),
algorithm: format!("{:?}", secret.key_type()),
})
}
None => Err(KeyStoreError::NoKeyEntry(alias.to_string())),
},
}
}
pub fn get_certificate_chain(&self, alias: &str) -> Result<Vec<Vec<u8>>, KeyStoreError> {
match self {
KeyStore::Jks(store) => match store.get_private_key_entry_certificate_chain(alias) {
Ok(chain) => Ok(chain.into_iter().map(|c| c.content).collect()),
Err(jks::KeyStoreError::EntryNotFound) => {
Err(KeyStoreError::NoKeyEntry(alias.to_string()))
}
Err(jks::KeyStoreError::WrongEntryType) => {
Ok(Vec::new())
}
Err(e) => Err(KeyStoreError::Malformed(format!(
"Failed to read the certificate chain of \"{}\": {}",
alias, e
))),
},
KeyStore::Pkcs12(store) => match store.entry(alias) {
Some(p12_keystore::KeyStoreEntry::PrivateKeyChain(chain)) => {
Ok(chain.certs().iter().map(|c| c.as_der().to_vec()).collect())
}
Some(p12_keystore::KeyStoreEntry::Certificate(cert)) => {
Ok(vec![cert.as_der().to_vec()])
}
Some(p12_keystore::KeyStoreEntry::Secret(_)) | None => Ok(Vec::new()),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_names_resolve_like_key_store_get_instance() {
assert_eq!(
KeyStoreFormat::from_name("JKS").unwrap(),
KeyStoreFormat::Jks
);
assert_eq!(
KeyStoreFormat::from_name("jks").unwrap(),
KeyStoreFormat::Jks
);
assert_eq!(
KeyStoreFormat::from_name("PKCS12").unwrap(),
KeyStoreFormat::Pkcs12
);
assert_eq!(
KeyStoreFormat::from_name("pkcs#12").unwrap(),
KeyStoreFormat::Pkcs12
);
assert_eq!(
KeyStoreFormat::from_name("pkcs12").unwrap(),
KeyStoreFormat::Pkcs12
);
assert!(matches!(
KeyStoreFormat::from_name("JCEKS"),
Err(KeyStoreError::UnsupportedType(_))
));
}
#[test]
fn format_is_detected_from_the_first_bytes() {
let mut jks = JKS_MAGIC.to_be_bytes().to_vec();
jks.extend_from_slice(&[0u8; 8]);
assert_eq!(KeyStoreFormat::detect(&jks), Some(KeyStoreFormat::Jks));
let asn1 = vec![0x30, 0x82, 0x01, 0x00];
assert_eq!(KeyStoreFormat::detect(&asn1), Some(KeyStoreFormat::Pkcs12));
assert_eq!(KeyStoreFormat::detect(&[0x00, 0x01, 0x02, 0x03]), None);
assert_eq!(KeyStoreFormat::detect(&[]), None);
}
#[test]
fn loading_without_passwords_reports_the_java_runtime_exception() {
let err = KeyStore::load(&[], &[], KeyStoreFormat::Jks).unwrap_err();
assert_eq!(err.to_string(), "No keystore passwords");
}
#[test]
fn a_wrong_keystore_password_is_reported() {
let data = JKS_MAGIC.to_be_bytes().to_vec();
let err = KeyStore::load(&data, &[vec!['x']], KeyStoreFormat::Jks).unwrap_err();
assert!(matches!(err, KeyStoreError::WrongKeystorePassword(_)));
}
#[test]
fn a_not_a_private_key_error_names_the_alias_and_algorithm() {
let err = KeyStoreError::NotAPrivateKey {
alias: "secret".to_string(),
algorithm: "Aes".to_string(),
};
let rendered = err.to_string();
assert!(rendered.contains("entry \"secret\""));
assert!(rendered.contains("algorithm: Aes"));
}
#[test]
fn key_entry_errors_name_the_alias() {
assert_eq!(
KeyStoreError::NoKeyEntry("release".to_string()).to_string(),
"entry \"release\" does not contain a key"
);
}
#[test]
fn an_unsupported_type_is_reported_without_touching_the_file() {
let err =
KeyStore::load_from_file("/nonexistent/keystore.bks", &[vec!['x'; 8]], Some("BKS"))
.unwrap_err();
assert!(matches!(err, KeyStoreError::UnsupportedType(_)));
}
#[test]
fn a_missing_keystore_file_is_reported() {
let err = KeyStore::load_from_file("/nonexistent/release.jks", &[vec!['x'; 8]], None)
.unwrap_err();
assert!(matches!(err, KeyStoreError::Malformed(_)));
}
#[test]
fn a_jks_store_round_trips_through_file_loading() {
use crate::password_retriever::Password;
const PASSWORD: &str = "pass123";
const KEY_BYTES: [u8; 5] = [0x30, 0x03, 0x02, 0x01, 0x2a];
let mut store = jks::KeyStore::new();
store
.set_private_key_entry(
"k",
jks::PrivateKeyEntry {
creation_time: std::time::SystemTime::UNIX_EPOCH,
private_key: KEY_BYTES.to_vec(),
certificate_chain: Vec::new(),
},
PASSWORD.as_bytes(),
)
.expect("write entry");
let mut encoded = Vec::new();
store
.store(&mut encoded, PASSWORD.as_bytes())
.expect("encode store");
let path = std::env::temp_dir().join("apksigner_keystore_round_trip.jks");
std::fs::write(&path, &encoded).expect("write file");
let passwords: Vec<Password> = vec![PASSWORD.chars().collect()];
let loaded =
KeyStore::load_from_file(path.to_str().unwrap(), &passwords, None).expect("load");
assert_eq!(loaded.aliases(), vec!["k".to_string()]);
assert!(loaded.is_key_entry("k"));
assert_eq!(loaded.get_key("k", &passwords).unwrap(), KEY_BYTES.to_vec());
assert!(loaded.get_certificate_chain("k").unwrap().is_empty());
assert!(matches!(
loaded.get_key("k", &[vec!['w'; 8]]),
Err(KeyStoreError::UnrecoverableKey(_))
));
std::fs::remove_file(&path).ok();
}
#[test]
fn an_explicit_type_overrides_detection() {
let data = JKS_MAGIC.to_be_bytes().to_vec();
let err = KeyStore::load(&data, &[vec!['x'; 8]], KeyStoreFormat::Pkcs12).unwrap_err();
assert!(matches!(err, KeyStoreError::WrongKeystorePassword(_)));
}
}