/// <summary> /// Returns the contents of the APK's <code>AndroidManifest.xml</code> or <code>null</code> if this entry /// is not present in the APK. /// </summary> /// <param name="cdRecords"></param> /// <param name="lhfSection"></param> /// <returns></returns> private static byte[] GetAndroidManifestFromApk(List <CentralDirectoryRecord> cdRecords, DataSource lhfSection) { var androidManifestCdRecord = cdRecords.FirstOrDefault(r => r.Name == ApkUtils.AndroidManifestZipEntryName); if (androidManifestCdRecord == null) { return(null); } return(LocalFileRecord.GetUncompressedData(lhfSection, androidManifestCdRecord, lhfSection.Length)); }
private static int GetInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { if (entry.IsDataCompressed) { // Compressed entries don't need to be aligned return(1); } // Attempt to obtain the alignment multiple from the entry's extra field. var extra = entry.Extra; if (extra.Length > 0) { // FORMAT: sequence of fields. Each field consists of: // * uint16 ID // * uint16 size // * 'size' bytes: payload using (var reader = new BinaryReader(new MemoryStream(extra))) { while ((reader.BaseStream.Remaining()) >= 4) { var headerId = reader.ReadInt16(); int dataSize = reader.ReadUInt16(); if (dataSize > (reader.BaseStream.Remaining())) { // Malformed field -- insufficient input remaining break; } if (headerId != AlignmentZipExtraDataFieldHeaderId) { // Skip this field reader.BaseStream.Position += dataSize; continue; } // This is APK alignment field. // FORMAT: // * uint16 alignment multiple (in bytes) // * remaining bytes -- padding to achieve alignment of data which starts after // the extra field if (dataSize < 2) { // Malformed break; } return(reader.ReadUInt16()); } } } // Fall back to filename-based defaults return((entry.Name.EndsWith(".so")) ? 4096 : 4); }
private static void FulfillInspectInputJarEntryRequest( DataSource lfhSection, LocalFileRecord localFileRecord, DefaultApkSignerEngine.IInspectJarEntryRequest inspectEntryRequest) { try { localFileRecord.OutputUncompressedData(lfhSection, inspectEntryRequest.DataSink); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.Name, e); } inspectEntryRequest.Done(); }
private static long OutputInputJarEntryLfhRecordPreservingDataAlignment( DataSource inputLfhSection, LocalFileRecord inputRecord, Stream outputLfhSection, long outputOffset) { var inputOffset = inputRecord.StartOffsetInArchive; if (inputOffset == outputOffset) { // This record's data will be aligned same as in the input APK. return(inputRecord.OutputRecord(inputLfhSection, outputLfhSection)); } var dataAlignmentMultiple = GetInputJarEntryDataAlignmentMultiple(inputRecord); if ((dataAlignmentMultiple <= 1) || ((inputOffset % dataAlignmentMultiple) == (outputOffset % dataAlignmentMultiple))) { // This record's data will be aligned same as in the input APK. return(inputRecord.OutputRecord(inputLfhSection, outputLfhSection)); } var inputDataStartOffset = inputOffset + inputRecord.DataStartOffset; if ((inputDataStartOffset % dataAlignmentMultiple) != 0) { // This record's data is not aligned in the input APK. No need to align it in the // output. return(inputRecord.OutputRecord(inputLfhSection, outputLfhSection)); } // This record's data needs to be re-aligned in the output. This is achieved using the // record's extra field. var aligningExtra = CreateExtraFieldToAlignData( inputRecord.Extra, outputOffset + inputRecord.ExtraFieldStartOffsetInsideRecord, dataAlignmentMultiple); return(inputRecord.OutputRecordWithModifiedExtra(inputLfhSection, aligningExtra, outputLfhSection)); }
/// <summary> /// Signs the input APK and outputs the resulting signed APK. The input APK is not modified. /// </summary> public void Sign() { // Step 1. Find input APK's main ZIP sections using (var inputApk = new DataSource(File.OpenRead(_inputFile))) using (var outputApk = new FileStream(_outputFile, FileMode.Create, FileAccess.ReadWrite)) { ApkUtils.ZipSections inputZipSections; try { inputZipSections = ApkUtils.FindZipSections(inputApk); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed APK: not a ZIP archive", e); } long inputApkSigningBlockOffset = -1; DataSource inputApkSigningBlock = null; var apkSigningBlockAndOffset = V2SchemeVerifier.FindApkSigningBlock(inputApk, inputZipSections); if (apkSigningBlockAndOffset != null) { inputApkSigningBlock = apkSigningBlockAndOffset.Item1; inputApkSigningBlockOffset = apkSigningBlockAndOffset.Item2; } var inputApkLfhSection = inputApk.Slice(0, (inputApkSigningBlockOffset != -1) ? inputApkSigningBlockOffset : inputZipSections.CentralDirectoryOffset); // Step 2. Parse the input APK's ZIP Central Directory var inputCd = GetZipCentralDirectory(inputApk, inputZipSections); var inputCdRecords = ParseZipCentralDirectory(inputCd, inputZipSections); // Step 3. Obtain a signer engine instance // Construct a signer engine from the provided parameters // Need to extract minSdkVersion from the APK's AndroidManifest.xml var minSdkVersion = GetMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection); var signerEngine = new DefaultApkSignerEngine(_certificate, minSdkVersion, V1SigningEnabled, V2SigningEnabled, DigestAlgorithm); // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any) if (inputApkSigningBlock != null) { signerEngine.InputApkSigningBlock(inputApkSigningBlock); } // Step 5. Iterate over input APK's entries and output the Local File Header + data of those // entries which need to be output. Entries are iterated in the order in which their Local // File Header records are stored in the file. This is to achieve better data locality in // case Central Directory entries are in the wrong order. var inputCdRecordsSortedByLfhOffset = new List <CentralDirectoryRecord>(inputCdRecords); inputCdRecordsSortedByLfhOffset.Sort(CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); var lastModifiedDateForNewEntries = -1; var lastModifiedTimeForNewEntries = -1; long inputOffset = 0; long outputOffset = 0; var outputCdRecordsByName = new Dictionary <string, CentralDirectoryRecord>(inputCdRecords.Count); foreach (var inputCdRecord in inputCdRecordsSortedByLfhOffset) { var entryName = inputCdRecord.Name; var entryInstructions = signerEngine.InputJarEntry(entryName); bool shouldOutput; switch (entryInstructions.OutputPolicy) { case DefaultApkSignerEngine.OutputPolicy.Output: shouldOutput = true; break; case DefaultApkSignerEngine.OutputPolicy.OutputByEngine: case DefaultApkSignerEngine.OutputPolicy.Skip: shouldOutput = false; break; default: throw new ArgumentOutOfRangeException( "Unknown output policy: " + entryInstructions.OutputPolicy); } var inputLocalFileHeaderStartOffset = inputCdRecord.LocalFileHeaderOffset; if (inputLocalFileHeaderStartOffset > inputOffset) { // Unprocessed data in input starting at inputOffset and ending and the start of // this record's LFH. We output this data verbatim because this signer is supposed // to preserve as much of input as possible. var chunkSize = inputLocalFileHeaderStartOffset - inputOffset; inputApkLfhSection.Feed(inputOffset, chunkSize, outputApk); outputOffset += chunkSize; inputOffset = inputLocalFileHeaderStartOffset; } LocalFileRecord inputLocalFileRecord; try { inputLocalFileRecord = LocalFileRecord.GetRecord( inputApkLfhSection, inputCdRecord, inputApkLfhSection.Length); } catch (ZipFormatException e) { throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.Name, e); } inputOffset += inputLocalFileRecord.Size; var inspectEntryRequest = entryInstructions.InspectJarEntryRequest; if (inspectEntryRequest != null) { FulfillInspectInputJarEntryRequest( inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); } if (shouldOutput) { // Find the max value of last modified, to be used for new entries added by the // signer. var lastModifiedDate = inputCdRecord.LastModificationDate; var lastModifiedTime = inputCdRecord.LastModificationTime; if ((lastModifiedDateForNewEntries == -1) || (lastModifiedDate > lastModifiedDateForNewEntries) || ((lastModifiedDate == lastModifiedDateForNewEntries) && (lastModifiedTime > lastModifiedTimeForNewEntries))) { lastModifiedDateForNewEntries = lastModifiedDate; lastModifiedTimeForNewEntries = lastModifiedTime; } inspectEntryRequest = signerEngine.OutputJarEntry(entryName); if (inspectEntryRequest != null) { FulfillInspectInputJarEntryRequest( inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); } // Output entry's Local File Header + data var outputLocalFileHeaderOffset = outputOffset; var outputLocalFileRecordSize = OutputInputJarEntryLfhRecordPreservingDataAlignment( inputApkLfhSection, inputLocalFileRecord, outputApk, outputLocalFileHeaderOffset); outputOffset += outputLocalFileRecordSize; // Enqueue entry's Central Directory record for output CentralDirectoryRecord outputCdRecord; if (outputLocalFileHeaderOffset == inputLocalFileRecord.StartOffsetInArchive) { outputCdRecord = inputCdRecord; } else { outputCdRecord = inputCdRecord.CreateWithModifiedLocalFileHeaderOffset( outputLocalFileHeaderOffset); } outputCdRecordsByName.Add(entryName, outputCdRecord); } } var inputLfhSectionSize = inputApkLfhSection.Length; if (inputOffset < inputLfhSectionSize) { // Unprocessed data in input starting at inputOffset and ending and the end of the input // APK's LFH section. We output this data verbatim because this signer is supposed // to preserve as much of input as possible. var chunkSize = inputLfhSectionSize - inputOffset; inputApkLfhSection.Feed(inputOffset, chunkSize, outputApk); outputOffset += chunkSize; inputOffset = inputLfhSectionSize; } // Step 6. Sort output APK's Central Directory records in the order in which they should // appear in the output var outputCdRecords = new List <CentralDirectoryRecord>(inputCdRecords.Count + 10); foreach (var inputCdRecord in inputCdRecords) { var entryName = inputCdRecord.Name; if (outputCdRecordsByName.TryGetValue(entryName, out var outputCdRecord)) { outputCdRecords.Add(outputCdRecord); } } // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File // Header + data entries and add to the list of output Central Directory records. var outputJarSignatureRequest = signerEngine.OutputJarEntries(); if (outputJarSignatureRequest != null) { if (lastModifiedDateForNewEntries == -1) { lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) lastModifiedTimeForNewEntries = 0; } foreach (var entry in outputJarSignatureRequest.AdditionalJarEntries) { var entryName = entry.Name; var uncompressedData = entry.Data; var deflateResult = ZipUtils.Deflate(uncompressedData); var compressedData = deflateResult.Item1; var uncompressedDataCrc32 = deflateResult.Item2; var inspectEntryRequest = signerEngine.OutputJarEntry(entryName); if (inspectEntryRequest != null) { inspectEntryRequest.DataSink.Write(uncompressedData, 0, uncompressedData.Length); inspectEntryRequest.Done(); } var localFileHeaderOffset = outputOffset; outputOffset += LocalFileRecord.OutputRecordWithDeflateCompressedData( entryName, lastModifiedTimeForNewEntries, lastModifiedDateForNewEntries, compressedData, uncompressedDataCrc32, uncompressedData.Length, outputApk); outputCdRecords.Add( CentralDirectoryRecord.CreateWithDeflateCompressedData( entryName, lastModifiedTimeForNewEntries, lastModifiedDateForNewEntries, uncompressedDataCrc32, compressedData.Length, uncompressedData.Length, localFileHeaderOffset)); } outputJarSignatureRequest.Done(); } // Step 8. Construct output ZIP Central Directory in an in-memory buffer long outputCentralDirSizeBytes = 0; foreach (var record in outputCdRecords) { outputCentralDirSizeBytes += record.Size; } var outputCentralDir = new MemoryStream((int)outputCentralDirSizeBytes); foreach (var record in outputCdRecords) { record.CopyTo(outputCentralDir); } var outputCentralDirStartOffset = outputOffset; var outputCentralDirRecordCount = outputCdRecords.Count; // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer var outputEocdBytes = EocdRecord.CreateWithModifiedCentralDirectoryInfo( inputZipSections.EndOfCentralDirectory, outputCentralDirRecordCount, outputCentralDir.Length, outputCentralDirStartOffset); var outputEocd = new MemoryStream(outputEocdBytes, true); // Step 10. Generate and output APK Signature Scheme v2 signatures, if necessary. This may // insert an APK Signing Block just before the output's ZIP Central Directory var outputApkSigingBlockRequest = signerEngine.OutputZipSections( outputApk, outputCentralDir, outputEocd); outputApk.Position = outputCentralDirStartOffset; if (outputApkSigingBlockRequest != null) { var outputApkSigningBlock = outputApkSigingBlockRequest.ApkSigningBlock; outputApk.Write(outputApkSigningBlock, 0, outputApkSigningBlock.Length); ZipUtils.SetZipEocdCentralDirectoryOffset( outputEocd, outputCentralDirStartOffset + outputApkSigningBlock.Length); outputApkSigingBlockRequest.Done(); } // Step 11. Output ZIP Central Directory and ZIP End of Central Directory outputCentralDir.Position = 0; outputCentralDir.CopyTo(outputApk); outputApk.Write(outputEocdBytes, 0, outputEocdBytes.Length); signerEngine.OutputDone(); } }