apksigner 1.0.0

APK signer and verifier CLI — a faithful Rust port of AOSP apksigner
//! PKCS #8 private-key loading for [`crate::signer_params::SignerParams`].
//!
//! Mirrors the two private-key entry points of
//! `com.android.apksigner.SignerParams`: `loadPrivateKeyFromFile` (which has to decide whether the
//! blob it read is an *encrypted* PKCS #8 `EncryptedPrivateKeyInfo` or a plain `PrivateKeyInfo`)
//! and `loadPkcs8EncodedPrivateKey` (which has to work out which key algorithm the blob holds).
//!
//! # Mapping notes
//!
//! * Java decides "is this encrypted?" by trying `new javax.crypto.EncryptedPrivateKeyInfo(blob)`
//!   and catching the `IOException` it throws for anything else. A plain `PrivateKeyInfo` is
//!   `SEQUENCE { INTEGER version, AlgorithmIdentifier, OCTET STRING }`, whose leading `INTEGER`
//!   cannot be read as the `EncryptionScheme` CHOICE, so the same probe distinguishes the two
//!   here.
//! * Password characters are converted to octets with UTF-8, which is what the JCA's PKCS #5
//!   implementations (`PBEKeySpec` → `PKCS5PasswordToBytes`/`PKCS5PasswordToUTF8Bytes`) use for
//!   PBES2.
//! * Java's `SecretKeyFactory`/`EncryptedPrivateKeyInfo.getKeySpec` handles both PBES1 and PBES2.
//!   The `pkcs8` crate's `encryption` feature enables PBES2 only, so a PBES1-protected key is
//!   reported as an unsupported encryption scheme rather than decrypted.
//! * Java's `loadPkcs8EncodedPrivateKey` tries `KeyFactory` for RSA, EC and DSA in turn. This
//!   port's [`apksig::key_config::PrivateKey`] has no DSA variant, so DSA blobs end in the same
//!   "Not an RSA, EC, or DSA private key" failure that Java reports for an unknown algorithm.

use std::fmt;

use crate::password_retriever::Password;

/// Errors raised while loading a PKCS #8 private key.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrivateKeyLoadError {
    /// The blob is not an `EncryptedPrivateKeyInfo`. Mirrors the `IOException` that
    /// `new javax.crypto.EncryptedPrivateKeyInfo(byte[])` throws for such input.
    NotEncryptedPrivateKey(String),
    /// Any other failure, carried as its message.
    Other(String),
}

impl fmt::Display for PrivateKeyLoadError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PrivateKeyLoadError::NotEncryptedPrivateKey(m) => write!(f, "{}", m),
            PrivateKeyLoadError::Other(m) => write!(f, "{}", m),
        }
    }
}

impl std::error::Error for PrivateKeyLoadError {}

/// Converts a password's characters to octets, the way the JCA's PKCS #5 implementations do.
///
/// `PasswordRetriever` yields `char[]` sequences; the PBE implementation encodes them as UTF-8
/// before deriving the key.
pub fn password_to_bytes(password: &[char]) -> Vec<u8> {
    let mut out = Vec::with_capacity(password.len());
    for &c in password {
        let mut buf = [0u8; 4];
        out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
    }
    out
}

/// Returns whether `blob` is a PKCS #8 `EncryptedPrivateKeyInfo`.
///
/// Mirrors the `try { new EncryptedPrivateKeyInfo(blob); } catch (IOException e)` probe in
/// `SignerParams.loadPrivateKeyFromFile`: `true` means the blob is encrypted, `false` means it
/// should be treated as a plain `PrivateKeyInfo`.
pub fn is_encrypted_private_key_info(blob: &[u8]) -> bool {
    pkcs8::EncryptedPrivateKeyInfo::try_from(blob).is_ok()
}

/// Decrypts a PKCS #8 `EncryptedPrivateKeyInfo`.
///
/// Mirrors `SignerParams.decryptPkcs8EncodedKey`: every candidate password is tried in order and
/// the first that yields the key wins. Returns the decrypted `PrivateKeyInfo` in DER form.
///
/// Java distinguishes `InvalidKeySpecException` (wrong password) from `InvalidKeyException`; the
/// RustCrypto PBES2 implementation reports both the same way, so the last failure's text is what
/// is surfaced.
pub fn decrypt_pkcs8_encoded_key(
    encrypted_private_key_info: &[u8],
    passwords: &[Password],
) -> Result<Vec<u8>, PrivateKeyLoadError> {
    let info = pkcs8::EncryptedPrivateKeyInfo::try_from(encrypted_private_key_info)
        .map_err(|e| PrivateKeyLoadError::NotEncryptedPrivateKey(e.to_string()))?;

    if passwords.is_empty() {
        // Mirrors the Java `RuntimeException("No passwords")`.
        return Err(PrivateKeyLoadError::Other("No passwords".to_string()));
    }

    let mut last_failure: Option<String> = None;
    for password in passwords {
        match info.decrypt(password_to_bytes(password)) {
            Ok(document) => return Ok(document.as_bytes().to_vec()),
            Err(e) => last_failure = Some(e.to_string()),
        }
    }
    Err(PrivateKeyLoadError::Other(
        last_failure.expect("at least one password was tried"),
    ))
}

/// Loads a private key from its unencrypted PKCS #8 encoding.
///
/// Mirrors `SignerParams.loadPkcs8EncodedPrivateKey`, which tries `KeyFactory.getInstance("RSA")`,
/// then `"EC"`, then `"DSA"` and finally reports `"Not an RSA, EC, or DSA private key"`.
///
/// The returned key is the port's canonical [`apksig::key_config::PrivateKey`], which is what
/// `ApkSigner` and the scheme signers consume.
pub fn load_pkcs8_encoded_private_key(
    pkcs8_der: &[u8],
) -> Result<apksig::key_config::PrivateKey, String> {
    use pkcs8::DecodePrivateKey;

    if let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der) {
        return Ok(apksig::key_config::PrivateKey::Rsa(Box::new(key)));
    }
    // The JCA picks the EC curve from the key's own parameters; each curve this port supports is
    // tried in turn. Each parser rejects a key whose declared curve is not its own, so the order
    // does not change the outcome.
    if let Ok(key) = p256::ecdsa::SigningKey::from_pkcs8_der(pkcs8_der) {
        return Ok(apksig::key_config::PrivateKey::EcP256(Box::new(key)));
    }
    if let Ok(key) = p384::ecdsa::SigningKey::from_pkcs8_der(pkcs8_der) {
        return Ok(apksig::key_config::PrivateKey::EcP384(Box::new(key)));
    }
    // `p521::ecdsa::SigningKey` carries no PKCS#8 parser of its own, so the scalar is read through
    // `p521::SecretKey` (the `elliptic_curve` secret key, which implements `DecodePrivateKey`) and
    // the signing key is rebuilt from its scalar -- the same route the v1 signer takes.
    if let Ok(secret) = p521::SecretKey::from_pkcs8_der(pkcs8_der)
        && let Ok(key) = p521::ecdsa::SigningKey::from_slice(&secret.to_bytes())
    {
        return Ok(apksig::key_config::PrivateKey::EcP521(Box::new(key)));
    }
    // DSA last, matching the JCA `KeyFactory` order. The port parses the key itself because the
    // `dsa` crate's key type is generic over the parameter size and cannot sign with the digest
    // apksig selects; see `apksig::internal::dsa`.
    if let Ok(key) = apksig::internal::dsa::DsaPrivateKey::from_pkcs8_der(pkcs8_der) {
        return Ok(apksig::key_config::PrivateKey::Dsa(Box::new(key)));
    }
    Err("Not an RSA, EC, or DSA private key".to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A deterministic P-256 key, so the tests stay fast and reproducible.
    fn p256_pkcs8_der() -> Vec<u8> {
        use pkcs8::EncodePrivateKey;
        let scalar = [7u8; 32];
        let key = p256::ecdsa::SigningKey::from_bytes(&scalar.into()).expect("valid scalar");
        key.to_pkcs8_der().expect("PKCS #8").as_bytes().to_vec()
    }

    #[test]
    fn password_characters_become_utf8_octets() {
        assert_eq!(
            password_to_bytes(&"hunter2".chars().collect::<Vec<_>>()),
            b"hunter2"
        );
        // A character outside US-ASCII becomes its multi-byte UTF-8 encoding.
        assert_eq!(password_to_bytes(&['\u{00e4}']), vec![0xc3, 0xa4]);
        assert!(password_to_bytes(&[]).is_empty());
    }

    #[test]
    fn a_plain_pkcs8_key_is_not_mistaken_for_an_encrypted_one() {
        // `EncryptedPrivateKeyInfo` starts with an `AlgorithmIdentifier`; a plain `PrivateKeyInfo`
        // starts with `INTEGER version`, so the probe must fail.
        assert!(!is_encrypted_private_key_info(&p256_pkcs8_der()));
    }

    #[test]
    fn garbage_is_not_an_encrypted_private_key() {
        assert!(!is_encrypted_private_key_info(&[0x00, 0x01, 0x02]));
        assert!(!is_encrypted_private_key_info(&[]));
        // A bare SEQUENCE with too little inside it is not one either.
        assert!(!is_encrypted_private_key_info(&[0x30, 0x01, 0x00]));
    }

    #[test]
    fn decrypting_without_passwords_reports_the_java_runtime_exception() {
        // A syntactically valid (if meaningless) `EncryptedPrivateKeyInfo` is needed to get past
        // the parse; the empty password list is then rejected.
        let info = vec![
            0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
        ];
        let err = decrypt_pkcs8_encoded_key(&info, &[]).unwrap_err();
        // Either the probe or the empty-password check rejects it; both are failures, and only
        // the latter is reachable with a real PBES2 blob.
        assert!(matches!(
            err,
            PrivateKeyLoadError::NotEncryptedPrivateKey(_) | PrivateKeyLoadError::Other(_)
        ));
    }

    #[test]
    fn an_unencrypted_blob_is_rejected_by_the_decryptor() {
        let err = decrypt_pkcs8_encoded_key(&p256_pkcs8_der(), &[vec!['x']]).unwrap_err();
        assert!(matches!(
            err,
            PrivateKeyLoadError::NotEncryptedPrivateKey(_)
        ));
    }

    #[test]
    fn a_p256_key_loads_as_an_ec_key() {
        let key = load_pkcs8_encoded_private_key(&p256_pkcs8_der()).expect("P-256 key");
        assert!(matches!(key, apksig::key_config::PrivateKey::EcP256(_)));
        assert_eq!(key.key_algorithm(), "EC");
    }

    /// P-521 keys are a distinct EC curve, and one the JCA accepts: a key that reaches the port
    /// has to be recognised as P-521 rather than rejected as "not an RSA, EC, or DSA" key.
    #[test]
    fn a_p521_key_loads_as_an_ec_key() {
        use pkcs8::EncodePrivateKey;

        // `p521::ecdsa::SigningKey` has no `EncodePrivateKey`, so the PKCS #8 blob is built from
        // the `elliptic_curve` secret key -- which is also the type the loader reads it through.
        // A P-521 scalar must be under the 521-bit group order, so the leading byte stays clear.
        let mut scalar = [0x11u8; 66];
        scalar[0] = 0;
        let secret = p521::SecretKey::from_slice(&scalar).expect("valid scalar");
        let der = secret.to_pkcs8_der().expect("PKCS #8");

        let loaded = load_pkcs8_encoded_private_key(der.as_bytes()).expect("P-521 key");
        assert!(matches!(loaded, apksig::key_config::PrivateKey::EcP521(_)));
        assert_eq!(loaded.key_algorithm(), "EC");
        // The rebuilt signing key must be usable, which is what proves the scalar survived the
        // round trip through `p521::ecdsa::SigningKey::from_slice`.
        assert!(!loaded.public_key_spki().expect("SPKI").is_empty());
    }

    #[test]
    fn an_unrecognised_blob_reports_the_java_algorithm_message() {
        let err = load_pkcs8_encoded_private_key(&[0x30, 0x01, 0x00]).unwrap_err();
        assert_eq!(err, "Not an RSA, EC, or DSA private key");
    }

    #[test]
    fn a_p384_key_is_distinguished_from_a_p256_key() {
        use pkcs8::EncodePrivateKey;
        let scalar = [9u8; 48];
        let key = p384::ecdsa::SigningKey::from_bytes(&scalar.into()).expect("valid scalar");
        let der = key.to_pkcs8_der().expect("PKCS #8").as_bytes().to_vec();
        let loaded = load_pkcs8_encoded_private_key(&der).expect("P-384 key");
        assert!(matches!(loaded, apksig::key_config::PrivateKey::EcP384(_)));
    }
}