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};
#[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 }
}
pub fn with_signer(mut self, identity: SigningIdentity) -> Self {
self.identities.push(identity);
self
}
pub fn min_sdk(mut self, min_sdk: ApiLevel) -> Self {
self.min_sdk = Some(min_sdk);
self
}
pub fn schemes(mut self, schemes: Schemes) -> Self {
self.schemes = Some(schemes);
self
}
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
}
pub fn sign_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
self.sign(path.as_ref(), path.as_ref())
}
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)?)?;
drop(archive);
drop(source);
staged.persist(output)
}
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 });
}
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 })
}
}
pub(crate) struct Plan<'a> {
identities: &'a [SigningIdentity],
schemes: Schemes,
jar_digest: DigestAlgorithm,
}
impl Plan<'_> {
pub fn jar_digest(&self) -> Option<DigestAlgorithm> {
self.schemes.contains(Schemes::V1).then_some(self.jar_digest)
}
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)
}
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)
}
}
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())
}
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(())
}
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(¢ral_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(())
}