安卓防簽名策略
安卓打包過程可參考google給出的APK打包流程圖,最終通過apkbuilder生成的apk實際上最終的儲存就是一個zip壓縮包,因此可以參考zip壓縮包的儲存格式來理解apk的儲存;當然apk打包前已經做了二進位制處理、資源壓縮、dex轉換等操作。
2、ZipAlign
經過aapt編譯生成的APK,實際上是一個有內部檔案規範的zip壓縮包;可以通過使用ZipAlign 命令確保所有未壓縮的資料的開頭均相對於檔案開頭部分執行特定的位元組對齊,這樣可減少應用消耗的 RAM 量;但是由於需要對資料採用邊界對齊,apk包的體積會增大,大約增加了100KB左右;
二、jarsigner簽名工具
1、v1簽名方案
jarSigner簽名方式由JDK提供,jarSigner簽名後生成一個META-INF資料夾。 1、MANIFEST.MF檔案,這個檔案包含了APK壓縮後的所有檔案對應的摘要資訊,每個檔案路徑和對應的摘要資訊都列舉出來:
Name: lib/armeabi/libNativeCrashCollect.so SHA-256-Digest: VAlz6QwJBoJ1mFMJTuzeA9sZ6m8e1QNGvE/KJ6iSa2c= Name: res/drawable/upgrade_progress.xml SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho= 複製程式碼
2、CERT.SF檔案,SF則是MF檔案的摘要資訊以及.MF檔案當中每個條目在用摘要演算法計算得到的摘要資訊並用base64編碼儲存;; 3、CERT.RSA,CERT.SF檔案則存放證書資訊,公鑰資訊,以及用私鑰對.SF檔案的加密資料即簽名信息;
2、APK安裝安卓驗證jarsigner簽名
使用jarsigner簽名的APK安裝時候,驗證可以參考sdk/sources/$sdkversion/android/util/jar 下面的檔案,驗證主要包括兩個部分,第一步通過CERT.RSA檔案驗證CERT.SF檔案,參考方法:
StrictJarVerifier.java synchronized boolean readCertificates(){ ...... while (it.hasNext()) { String key = it.next(); if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) { verifyCertificate(key); it.remove(); } } } static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)throws GeneralSecurityException { ...... } 複製程式碼
v1也支援多種簽名,以上只是通過解密驗證.SF檔案的摘要資訊是正確的;第二步是驗證.SF檔案.MF檔案對應的摘要資訊,確保META-INF目錄下的檔案沒有被篡改:
private void verifyCertificate(String certFile) { ...... byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME); // Manifest entry is required for any verifications. if (manifestBytes == null) { return; } ...... // Use .SF to verify the whole manifest. String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest"; if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) { Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Attributes> entry = it.next(); StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey()); if (chunk == null) { return; } if (!verify(entry.getValue(), "-Digest", manifestBytes, chunk.start, chunk.end, createdBySigntool, false)) { throw invalidDigest(signatureFile, entry.getKey(), jarName); } } } } 複製程式碼
上面做了兩個事情,第一個事情通過驗證.SF檔案和.MF的摘要來確認.MF檔案是沒有被篡改的,然後讀取.MF檔案對應的檔案摘要資訊,類似:
Name: lib/armeabi/libNativeCrashCollect.so SHA-256-Digest: VAlz6QwJBoJ1mFMJTuzeA9sZ6m8e1QNGvE/KJ6iSa2c= Name: res/drawable/upgrade_progress.xml SHA-256-Digest: GGArxKNKxUIsTCFsjmcGbOvrXLn8l9VfUfha2M9Znho= 複製程式碼
然後對應確認每一個檔案對應的摘要資訊是否是正確的,以此來確保APK解壓後的檔案都沒有被修改過;
3、jarsigner簽名的缺陷
根據上面安卓校驗jarsigner的過程,可以看到jarsigner簽名後的APK可能有如下問題: 1、對於META-INF資料夾,安卓只會校驗CERT.SF三個檔案,如若在META-INF存放其他檔案,會逃過安卓的檢測過程,此處存在較大的安全漏洞;(參考美團通過在META-INF下新增一個空檔案來代表渠道號) 2、每次安裝APK都需要通過解壓APK再做對應的校驗,解壓APK是一個耗時耗電的過程,安裝過程體驗不好;
三、APK Signature Scheme v2簽名
1、apksigner簽名
Android 7.0引入了新的應用簽名方案APK Signature Schemev2,APK簽名方案v2是基於APK二進位制檔案的,即簽名和安裝校驗都是基於APK二進位制檔案的,即只要二進位制檔案發生改變,就認為APK被修改了。 apksigner簽名前後APK檔案內容如下:
v2簽名後在Central Directory塊前生成一個APK Signing Block,儲存的就是v2簽名和簽名者身份資訊; apk簽名塊的結構如下
偏移 | 位元組數 | 描述 |
---|---|---|
@+0 | 8 | 這個Block的長度(本欄位的長度不計算在內) |
@+8 | n | 一組ID-value |
@-24 | 8 | 這個Block的長度(和第一個欄位一樣值) |
@-16 | 16 | 魔數"APK Sign Block 42" |
APK的v2簽名的ID-value可以儲存多個Id-值對,其中會被校驗的"ID-值"對的ID為0x7109871a ,其他ID未知的"ID-值"對不會被解譯;此處可以做為一個漏洞,美團新的渠道包方案就是利用了這個漏洞;通過分析安卓對於v2簽名檔案的原始碼可知,在簽名前,安卓生成的 APK是一個壓縮二進位制檔案,v2簽名後也會生成一個對應的SF檔案,SF檔案裡面有個標誌 X-Android-APK-Signed ,
判斷是否有v2簽名這個標誌,對應命令: apksigner verify 執行這個命令的原始碼其實就是:
java原始碼
/** * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates * associated with each signer. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not *verify. * @throws IOException if an I/O error occurs while reading the APK file. */ private static X509Certificate[][] verify(RandomAccessFile apk) throws SignatureNotFoundException, SecurityException, IOException { SignatureInfo signatureInfo = findSignature(apk); return verify(apk.getFD(), signatureInfo); } 複製程式碼
首先我們需要找到對應的APK Signing Block ,話不多說,直接看原始碼:
private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { ...... // Find the APK Signing Block. The block immediately precedes the Central Directory. long centralDirOffset = getCentralDirOffset(eocd, eocdOffset); Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset); ...... 複製程式碼
由上面原始碼邏輯可以看到,首先需要找到Central Directory,然後根據儲存結構找到前面的Signing Block,怎麼去確定是否有生成Signing Block呢?看程式碼是如何實現的?
private static Pair<ByteBuffer, Long> findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) throws IOException, SignatureNotFoundException { ...... // Read the magic and offset in file from the footer section of the block: // * uint64:size of block // * 16 bytes: magic ByteBuffer footer = ByteBuffer.allocate(24); footer.order(ByteOrder.LITTLE_ENDIAN); apk.seek(centralDirOffset - footer.capacity()); apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity()); if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { throw new SignatureNotFoundException( "No APK Signing Block before ZIP Central Directory"); } ...... 複製程式碼
需要關注如下兩個值:
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; 複製程式碼
如下真正去生成Apk Signing Block的程式碼需要結合Apk的二進位制小端序結構去分析,具體程式碼如下:
private static Pair<ByteBuffer, Long> findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) throws IOException, SignatureNotFoundException { ...... int totalSize = (int) (apkSigBlockSizeInFooter + 8); long apkSigBlockOffset = centralDirOffset - totalSize; if (apkSigBlockOffset < 0) { throw new SignatureNotFoundException( "APK Signing Block offset out of range: " + apkSigBlockOffset); } ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); apk.seek(apkSigBlockOffset); apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity()); long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); ...... 複製程式碼
APK Signing Block APK簽名分塊裡面儲存有 APK簽名方案V2分塊,關於其查詢過程,可以參考原始碼
java原始碼
private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { ...... // Find the APK Signature Scheme v2 Block inside the APK Signing Block. ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock); ...... 複製程式碼
APK Signing Block內部多位元組物件儲存方式採用的是LITTLE_ENDIAN小端序;一定要記得APK Signing Block內部的儲存內容,由於採用小端序,前面32個位元組的資料是固定的,用來儲存長度和Scheme v2分塊,由於用來儲存ID-Value的區域是不固定的,因此整個簽名分塊的長度是未知的,因此就有對應的標誌長度的欄位; 對應原始碼:
private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock) throws SignatureNotFoundException { checkByteOrderLittleEndian(apkSigningBlock); // FORMAT: // OFFSETDATA TYPEDESCRIPTION // * @+0bytes uint64:size in bytes (excluding this field) // * @+8bytes pairs // * @-24 bytes uint64:size in bytes (same as the one above) // * @-16 bytes uint128:magic ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); ...... if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { return getByteBuffer(pairs, len - 4); } ...... throw new SignatureNotFoundException( "No APK Signature Scheme v2 block in APK Signing Block"); } 複製程式碼
仔細觀察上面的程式碼,給的ByteBuffer的起始位置資訊,其實這是因為目前的儲存結構是小端序,因此實際給出的ByteBuffer就是去掉ID-Value剩下的值;小黑板:在v2簽名驗證過程中,用來儲存Id-Value的區間被過濾掉不做檢查,安卓在v2簽名也留下來給大家可以利用的區間;
為什麼會有兩個區域用來儲存簽名分塊的長度呢?尋找APK簽名方案v2分塊的過程是以ID:0x7109871a 為標誌,找到對應的value值,這個ID標誌位很重要,其他所有的value值都是根據這個ID索引得到的; 而這個APK Signature Scheme v2 Block儲存的資料signer由幾部分組成,第一個是signed data 儲存將APK內容按照一定規則分塊計算摘要,採用兩級樹方式,最終得到的摘要資訊;第二個是signatures 儲存當前簽名所採用的簽名演算法,目前可以支援的計算摘要演算法有7種,而對應的摘要演算法又有對應的加密演算法,因此這個欄位儲存了簽名演算法;第三個是帶長度字首的public key(SubjectPublicKeyInfo,ASN.1 DER 形式) ,即剛剛用來加密的私鑰對應的公鑰資訊;以上就是APK Signature Scheme v2 Block的資料儲存結構,可以直觀的看下圖:
APK資料是很大,如果直接採用非對稱加密資料,效果是非常慢的,那如何做簽名呢?—— 答案是對APK受保護的資料直接按照一定規則分塊,然後對分塊分塊計算摘要,再採用兩級樹方式,將剛剛得到的分塊摘要再按照一定規則計算得到最終摘要;非對稱加密直接私鑰加密最終摘要資訊;如上的簽名方案是否還有加快計算速度的方案?—— 可以先提前分塊,然後考慮並行處理計算分塊摘要,大大提高計算速度;
上面的過程對應原始碼實現:
private static X509Certificate[] verifySigner( ByteBuffer signerBlock, Map<Integer, byte[]> contentDigests, CertificateFactory certFactory) throws SecurityException, IOException { ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); ...... //get signature,consider litte-edian while (signatures.hasRemaining()) { signatureCount++; try { ByteBuffer signature = getLengthPrefixedSlice(signatures); ...... int sigAlgorithm = signature.getInt(); signaturesSigAlgorithms.add(sigAlgorithm); if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { continue; } if ((bestSigAlgorithm == -1) || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { bestSigAlgorithm = sigAlgorithm; bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); } } catch (IOException | BufferUnderflowException e) { ...... } } ...... //verify signed data try { PublicKey publicKey = KeyFactory.getInstance(keyAlgorithm) .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); Signature sig = Signature.getInstance(jcaSignatureAlgorithm); sig.initVerify(publicKey); if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } sig.update(signedData); sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException | SignatureException e) { throw new SecurityException( "Failed to verify " + jcaSignatureAlgorithm + " signature", e); } if (!sigVerified) { throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); } ...... //get digest of signed data while (digests.hasRemaining()) { digestCount++; try { ByteBuffer digest = getLengthPrefixedSlice(digests); if (digest.remaining() < 8) { throw new IOException("Record too short"); } int sigAlgorithm = digest.getInt(); digestsSigAlgorithms.add(sigAlgorithm); if (sigAlgorithm == bestSigAlgorithm) { contentDigest = readLengthPrefixedByteArray(digest); } } catch (IOException | BufferUnderflowException e) { throw new IOException("Failed to parse digest record #" + digestCount, e); } } ...... //verify digest int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm); byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest); if ((previousSignerDigest != null) && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) { throw new SecurityException( getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) + " contents digest does not match the digest specified by a preceding signer"); } ...... //get public key ByteBuffer certificates = getLengthPrefixedSlice(signedData); List<X509Certificate> certs = new ArrayList<>(); int certificateCount = 0; while (certificates.hasRemaining()) { certificateCount++; byte[] encodedCert = readLengthPrefixedByteArray(certificates); X509Certificate certificate; try { certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); } catch (CertificateException e) { throw new SecurityException("Failed to decode certificate #" + certificateCount, e); } certificate = new VerbatimX509Certificate(certificate, encodedCert); certs.add(certificate); } //verify public key if (certs.isEmpty()) { throw new SecurityException("No certificates listed"); } X509Certificate mainCertificate = certs.get(0); byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { throw new SecurityException( "Public key mismatch between certificate and signature record"); } return certs.toArray(new X509Certificate[certs.size()]); 複製程式碼
上面的原始碼比較多,但是大致的邏輯一樣,都是先得到對應的signature、digest of signed data、public Key,然後分別都要做verify;
上面做了分別verify之後,接下來要做完整性校驗,也就是驗證我們的簽名邏輯,直接看原始碼是如何處理的?
private static void verifyIntegrity( Map<Integer, byte[]> expectedDigests, FileDescriptor apkFileDescriptor, long apkSigningBlockOffset, long centralDirOffset, long eocdOffset, ByteBuffer eocdBuf) throws SecurityException { // We need to verify the integrity of the following three sections of the file: // 1. Everything up to the start of the APK Signing Block. // 2. ZIP Central Directory. // 3. ZIP End of Central Directory (EoCD). // Each of these sections is represented as a separate DataSource instance below. // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to // avoid wasting physical memory. In most APK verification scenarios, the contents of the // APK are already there in the OS's page cache and thus mmap does not use additional // physical memory. DataSource beforeApkSigningBlock = new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset); DataSource centralDir = new MemoryMappedFileDataSource( apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset); // For the purposes of integrity verification, ZIP End of Central Directory's field Start of // Central Directory must be considered to point to the offset of the APK Signing Block. eocdBuf = eocdBuf.duplicate(); eocdBuf.order(ByteOrder.LITTLE_ENDIAN); ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset); ...... //計算最終摘要 try { actualDigests = computeContentDigests( digestAlgorithms, new DataSource[] {beforeApkSigningBlock, centralDir, eocd}); } catch (DigestException e) { throw new SecurityException("Failed to compute digest(s) of contents", e); } ...... } 複製程式碼
computeContentDigests就是整個計算摘要的函式,具體原始碼可以自行閱讀,上面已經簡要說明其原理;如上就是APK安裝時候,安卓系統驗證是否使用v2簽名的過程;
從上面v2簽名的過程來看,其相對於jarsigner方式,想要做二次簽名就是需要熟悉v1簽名過程,考慮針對二進位制檔案去掉對應的APK Signing Block再重新簽名,實際上也是可以實現的; APKSigner想對於JarSigner的優先兩個:1、簽名更快,安裝時候驗證簽名也更快,直接對二進位制檔案操作,而不需要像jarsigner那樣需要先壓縮檔案簽名,先解壓檔案驗證簽名,效率太低;2、安全性更好,使用者想要抹掉簽名重新修改檔案的成本更高,需要對整個ApkSigner原理非常清楚,二次簽名的成本更高; 但是如上面所述,ApkSigner並不能防止二次簽名,要防二次簽名需要有其他方案;
v2分塊的本質就是數字簽名的過程,因此會儲存對應的加解密資訊和摘要資訊; 每一個APK簽名方案v2分塊對應一個簽名者/身份簽名,有多個簽名者則含有多個v2分塊;v2分塊結構資訊如下: v2分塊是用來保護APK全檔案的,明文即APK全檔案資訊,v2分塊儲存了摘要演算法和摘要資訊,同時儲存了數字證書資訊和加密演算法資訊,並提供公鑰資訊;類似數字簽名的校驗一致,最終通過計算就能夠證明APK已經做了v2簽名,且apk內容沒有被篡改; 目前APK Signature演算法支援的主流的摘要演算法,同時支援RSA、DSA、EC橢圓加密等非對稱演算法; APK Signature演算法實質是通過對APK全內容做類似數字簽名的工作,來保證APK檔案不會被篡改。
2、APK Signature保護APK內容的實現
首先打包的APK轉換成zip其檔案結構如下:?問題1:可以把APK當作Zip檔案來處理,但是Zip結構是有幾個限制條件的,比如zipcommentfield對應到什麼內容,zip eocd會有 comment field?
APK Signature演算法主要做了兩個事情: a,計算第1、3、4部分內容的摘要,將這些摘要資訊儲存到APK Signing Block的v2分塊的signed data分塊;(?是對最終的頂級摘要加密簽名還是對每個分塊摘要都加密簽名————最終計算得到的頂級摘要資訊儲存到singed data分塊,因為最終只是按照規則計算最終摘要相同即可;) b,將上面得到的分塊(摘要資訊)通過一個或多個加密演算法來加密;(?對頂級摘要可能採用一個或多個簽名來保護————為了安全性考慮,可能採用多種加密簽名方式來保護,這個只是為了增加Signing Block資料的安全性而已;) 從上面的步驟可以看到,這實際上就是數字簽名的實現方式; 計算分塊的策略如下,先將資訊分成1MB的連續塊,然後分別計算每塊的摘要,可以通過並行處理加快計算速度;然後將得到的分塊摘要再按照規則計算得到最終的頂級摘要;
3、APK安裝驗證流程:
Google官方驗證APK流程圖: 驗證v2簽名的流程: 複製程式碼
1、先找到APK Signing Block,程式碼如下:
ApkSignatureSchemeV2Verifier.java public static X509Certificate[][] verify(String apkFile) throws SignatureNotFoundException, SecurityException, IOException { try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { return verify(apk); } } private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { ...... long centralDirOffset = getCentralDirOffset(eocd, eocdOffset); Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset); ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first; long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second; // Find the APK Signature Scheme v2 Block inside the APK Signing Block. ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock); ..... } 複製程式碼
2、對於v2分塊的每個signer做驗證,首先找到此signer所採用的加密演算法,然後對signed data做解密,確保得到了正確的摘要資訊,程式碼如下:
private static X509Certificate[][] verify( FileDescriptor apkFileDescriptor, SignatureInfo signatureInfo) throws SecurityException { ...... while (signers.hasRemaining()) { signerCount++; try { ByteBuffer signer = getLengthPrefixedSlice(signers); X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory); signerCerts.add(certs); } catch (IOException | BufferUnderflowException | SecurityException e) { throw new SecurityException( "Failed to parse/verify signer #" + signerCount + " block", e); } } ...... } 複製程式碼
3、然後驗證最終的摘要資訊是否正確,只要頂級摘要是正確的,表明摘要資訊就是沒有被篡改的,程式碼如下:
private static void verifyIntegrity( Map<Integer, byte[]> expectedDigests, FileDescriptor apkFileDescriptor, long apkSigningBlockOffset, long centralDirOffset, long eocdOffset, ByteBuffer eocdBuf) throws SecurityException { ...... try { actualDigests = computeContentDigests( digestAlgorithms, new DataSource[] {beforeApkSigningBlock, centralDir, eocd}); } catch (DigestException e) { throw new SecurityException("Failed to compute digest(s) of contents", e); } for (int i = 0; i < digestAlgorithms.length; i++) { int digestAlgorithm = digestAlgorithms[i]; byte[] expectedDigest = expectedDigests.get(digestAlgorithm); byte[] actualDigest = actualDigests[i]; if (!MessageDigest.isEqual(expectedDigest, actualDigest)) { throw new SecurityException( getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) + " digest of contents did not verify"); } } } private static byte[][] computeContentDigests( int[] digestAlgorithms, DataSource[] contents) throws DigestException { ...... for (DataSource input : contents) { long inputOffset = 0; long inputRemaining = input.size(); while (inputRemaining > 0) { int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES); setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); for (int i = 0; i < mds.length; i++) { mds[i].update(chunkContentPrefix); } ...... } for (int i = 0; i < digestAlgorithms.length; i++) { int digestAlgorithm = digestAlgorithms[i]; byte[] input = digestsOfChunks[i]; String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); MessageDigest md; try { md = MessageDigest.getInstance(jcaAlgorithmName); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); } byte[] output = md.digest(input); result[i] = output; } ...... } 複製程式碼