rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Signing APKs.

use std::fs::File;
use std::io::{BufWriter, Seek, SeekFrom, Write};
use std::path::Path;

use super::{
    BlockId, DigestAlgorithm, KeyKind, Scheme, Schemes, SignError, SigningBlock, SigningIdentity, jar, scheme,
};
use crate::archive::{self, ApkLayout, ApkWriter, EntryOptions, ReadAt, ZipError};
use crate::digest::{ContentDigest, content_digests};
use crate::error::{Error, FileOperation};
use crate::native::PageSize;
use crate::types::{ApiLevel, ArchivePath};

/// Signs APKs with one or more identities.
///
/// Like `apksigner`, the schemes default to [`Schemes::for_min_sdk`], and
/// the minimum and target API levels are read from the APK's manifest unless
/// the minimum is set explicitly.
#[derive(Clone, Debug)]
pub struct Signer {
    identities: Vec<SigningIdentity>,
    schemes: Option<Schemes>,
    min_sdk: Option<ApiLevel>,
    page_size: PageSize,
}

impl Signer {
    pub fn new(identity: SigningIdentity) -> Self {
        Self { identities: vec![identity], schemes: None, min_sdk: None, page_size: PageSize::SixteenKiB }
    }

    /// Adds another signer (v1 and v2 only).
    pub fn with_signer(mut self, identity: SigningIdentity) -> Self {
        self.identities.push(identity);
        self
    }

    /// The oldest Android release the APK must install on, overriding the
    /// manifest's `minSdkVersion`. This selects the default schemes and the
    /// JAR digest.
    pub fn min_sdk(mut self, min_sdk: ApiLevel) -> Self {
        self.min_sdk = Some(min_sdk);
        self
    }

    /// Overrides the default schemes.
    pub fn schemes(mut self, schemes: Schemes) -> Self {
        self.schemes = Some(schemes);
        self
    }

    /// The page size stored native libraries are aligned to when re-signing
    /// an existing APK (default 16 KiB).
    pub fn native_library_page_size(mut self, page_size: PageSize) -> Self {
        self.page_size = page_size;
        self
    }

    pub fn identities(&self) -> &[SigningIdentity] {
        &self.identities
    }

    /// Signs the APK at `path` in place. The file is replaced atomically, and
    /// left untouched if signing fails.
    pub fn sign_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
        self.sign(path.as_ref(), path.as_ref())
    }

    /// Signs `input` into `output` (which may be the same path). Existing
    /// signatures are replaced, stored entries are realigned (native
    /// libraries to pages, everything else to 4 bytes), and compressed data
    /// is copied without recompressing it.
    pub fn sign(&self, input: impl AsRef<Path>, output: impl AsRef<Path>) -> Result<(), Error> {
        let (input, output) = (input.as_ref(), output.as_ref());
        let source = File::open(input).map_err(Error::file(FileOperation::Open, input))?;
        let (mut archive, layout) = archive::open(&source)?;
        let manifest = crate::apk::read_manifest(&mut archive)?;
        let min_sdk = manifest.as_ref().map(|m| m.min_sdk().api_level()).transpose()?;
        let target_sdk = manifest.as_ref().map(|m| m.effective_target_sdk()).transpose()?;
        let plan = self.plan(min_sdk, target_sdk)?;

        let mut staged = crate::fs::Staged::beside(output)?;
        let staged_path = staged.path().to_owned();
        let mut writer = ApkWriter::new(BufWriter::new(staged.file()));
        if let Some(digest) = plan.jar_digest() {
            writer = writer.digest_entries(digest);
        }
        writer.set_comment(layout.comment())?;
        let page_size = self.page_size.bytes() as u16;
        for index in 0..archive.len() {
            let name = archive::entry_name(&archive, index)?;
            if archive::is_signature_file(&name) {
                continue;
            }
            let alignment = if name.starts_with("lib/") && name.ends_with(".so") { page_size } else { 4 };
            writer.copy(&mut archive, index, alignment)?;
        }
        for (path, data) in plan.jar_signature_files(writer.digests())? {
            writer.add(&path, &data, EntryOptions::STORED)?;
        }
        writer.finish()?.flush().map_err(Error::file(FileOperation::Write, staged_path))?;
        plan.apply_block(staged.file(), &preserved_pairs(&source, &layout)?)?;
        // Close the input before replacing it; Windows cannot rename over an open file.
        drop(archive);
        drop(source);
        staged.persist(output)
    }

    /// The schemes and digests for signing an app supporting `min_sdk`
    /// onwards (unless overridden) and targeting `target_sdk` (which
    /// defaults to the minimum).
    pub(crate) fn plan(&self, min_sdk: Option<ApiLevel>, target_sdk: Option<ApiLevel>) -> Result<Plan<'_>, SignError> {
        let min_sdk = self.min_sdk.or(min_sdk).ok_or(SignError::UnknownMinSdk)?;
        let schemes = self.schemes.unwrap_or_else(|| Schemes::for_min_sdk(min_sdk));
        schemes.check_coverage(min_sdk, target_sdk.unwrap_or(min_sdk))?;
        if schemes.contains(Schemes::V3) && self.identities.len() > 1 {
            return Err(SignError::MultipleV3Signers { count: self.identities.len() });
        }
        let has_ec_signer = self.identities.iter().any(|i| !matches!(i.key().kind(), KeyKind::Rsa { .. }));
        if schemes.contains(Schemes::V1) && min_sdk < ApiLevel::JELLY_BEAN_MR2 && has_ec_signer {
            return Err(SignError::EcdsaJarRequiresApi18 { min_sdk });
        }
        // Like apksigner: SHA-1 for apps that support API levels below 18
        // (or below 21 with EC keys, since earlier releases cannot verify
        // `SHA256withECDSA`), SHA-256 otherwise.
        let jar_digest = if min_sdk < ApiLevel::JELLY_BEAN_MR2 || (has_ec_signer && min_sdk < ApiLevel::LOLLIPOP) {
            DigestAlgorithm::Sha1
        } else {
            DigestAlgorithm::Sha256
        };
        Ok(Plan { identities: &self.identities, schemes, jar_digest })
    }
}

/// How one app is signed: the checked schemes and the JAR digest.
pub(crate) struct Plan<'a> {
    identities: &'a [SigningIdentity],
    schemes: Schemes,
    jar_digest: DigestAlgorithm,
}

impl Plan<'_> {
    /// The digest of JAR signatures, if they are written.
    pub fn jar_digest(&self) -> Option<DigestAlgorithm> {
        self.schemes.contains(Schemes::V1).then_some(self.jar_digest)
    }

    /// The JAR signature entries for the entries digested in `digests`, or
    /// none without v1. They declare the other schemes for stripping
    /// protection.
    pub fn jar_signature_files(&self, digests: &[(String, Vec<u8>)]) -> Result<Vec<(ArchivePath, Vec<u8>)>, SignError> {
        if !self.schemes.contains(Schemes::V1) {
            return Ok(Vec::new());
        }
        let declared: Vec<Scheme> = self.schemes.iter_schemes().filter(|s| *s != Scheme::V1).collect();
        jar::sign(digests, self.jar_digest, self.identities, &declared)
    }

    /// Inserts the v2/v3 signing block, carrying over `preserved` entries,
    /// into the otherwise complete APK in `file`.
    pub fn apply_block(&self, file: &mut File, preserved: &[(BlockId, Vec<u8>)]) -> Result<(), Error> {
        let block_schemes: Vec<Scheme> = self.schemes.iter_schemes().filter(|s| *s != Scheme::V1).collect();
        if block_schemes.is_empty() {
            return Ok(());
        }
        let layout = ApkLayout::read(&mut *file)?;
        let mut algorithms: Vec<ContentDigest> =
            self.identities.iter().filter_map(|i| i.key().signature_algorithm().content_digest()).collect();
        algorithms.sort();
        algorithms.dedup();
        let digests = content_digests(&*file, &layout, &algorithms)?;
        let mut block = SigningBlock::default();
        for &scheme in &block_schemes {
            let stronger: Vec<Scheme> = block_schemes.iter().copied().filter(|s| *s > scheme).collect();
            let value = scheme::sign(scheme, self.identities, &digests, &stronger)?;
            block.pairs.push((scheme.block_id().expect("block scheme"), value));
        }
        block.pairs.extend_from_slice(preserved);
        let block = block.encode();
        self_check(&block, &block_schemes).map_err(|e| SignError::SelfCheck(Box::new(e)))?;
        splice(file, &layout, &block)
    }
}

/// The entries of an APK's existing signing block that re-signing keeps,
/// such as Google Play's dependency metadata. Signatures are replaced.
fn preserved_pairs(source: &File, layout: &ApkLayout) -> Result<Vec<(BlockId, Vec<u8>)>, Error> {
    let Some(range) = &layout.signing_block else { return Ok(Vec::new()) };
    let bytes = source.read_vec_at(range.start, (range.end - range.start) as usize).map_err(ZipError::Io)?;
    let block = SigningBlock::parse(&bytes)?;
    Ok(block.pairs.into_iter().filter(|(id, _)| !id.is_signature_data()).collect())
}

/// Re-parses a freshly built signing block and verifies its signatures over
/// the signed data, catching key or encoding mistakes before writing.
fn self_check(block: &[u8], schemes: &[Scheme]) -> Result<(), super::VerifyError> {
    let block = SigningBlock::parse(block)?;
    for &scheme in schemes {
        let value = block.get(scheme.block_id().expect("block scheme")).expect("the block was built with every scheme");
        scheme::verify(scheme, value, &mut |_| Ok(None))?;
    }
    Ok(())
}

/// Writes `block` between the ZIP entries and the central directory.
fn splice(file: &mut File, layout: &ApkLayout, block: &[u8]) -> Result<(), Error> {
    let io = |error| Error::from(ZipError::Io(error));
    let cd = &layout.central_directory;
    let central_directory = file.read_vec_at(cd.start, (cd.end - cd.start) as usize).map_err(io)?;
    let eocd = layout.eocd_with_central_directory_at(layout.entries_end + block.len() as u64)?;
    let mut tail = Vec::with_capacity(block.len() + central_directory.len() + eocd.len());
    tail.extend_from_slice(block);
    tail.extend_from_slice(&central_directory);
    tail.extend_from_slice(&eocd);
    file.seek(SeekFrom::Start(layout.entries_end)).map_err(io)?;
    file.write_all(&tail).map_err(io)?;
    file.set_len(layout.entries_end + tail.len() as u64).map_err(io)?;
    Ok(())
}