rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Java keystores: JKS and PKCS#12 (the format `keytool` and Android Studio
//! create by default).

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> },
}

/// An unlocked JKS or PKCS#12 keystore.
///
/// ```no_run
/// use apk::sign::Keystore;
///
/// let keystore = Keystore::open("release.keystore", "store password")?;
/// let identity = keystore.identity("upload", None)?;
/// # Ok::<(), apk::Error>(())
/// ```
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 {
    /// Opens and unlocks the keystore at `path`.
    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)?)
    }

    /// Unlocks a keystore held in memory, detecting its format.
    pub fn from_bytes(bytes: &[u8], password: &str) -> Result<Self, KeyError> {
        // The parsers can panic on malformed input; treat that as a format error.
        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)?
    }

    /// The aliases of all entries.
    pub fn aliases(&self) -> Vec<String> {
        match &self.0 {
            Store::Jks { store, .. } => store.aliases(),
            Store::Pkcs12 { store, .. } => store.entries().map(|(alias, _)| alias.clone()).collect(),
        }
    }

    /// The private key entry `alias`. JKS entries may have their own
    /// password; it defaults to the store password. PKCS#12 entries always
    /// use the store password.
    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)
    }

    /// The identity of the keystore's only private key entry.
    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());
    }
}