blob: b23ccb8bf826e873ba4d9d7473d974b8eacc5258 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.pm;
import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256;
import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
import static android.content.pm.Checksum.TYPE_WHOLE_MD5;
import static android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256;
import static android.content.pm.Checksum.TYPE_WHOLE_SHA1;
import static android.content.pm.Checksum.TYPE_WHOLE_SHA256;
import static android.content.pm.Checksum.TYPE_WHOLE_SHA512;
import static android.content.pm.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION;
import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256;
import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApkChecksum;
import android.content.pm.Checksum;
import android.content.pm.IOnChecksumsReadyListener;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
import android.content.pm.SigningDetails.SignatureSchemeVersion;
import android.content.pm.parsing.ApkLiteParseUtils;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.incremental.IncrementalManager;
import android.os.incremental.IncrementalStorage;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;
import android.util.apk.ApkSignatureSchemeV2Verifier;
import android.util.apk.ApkSignatureSchemeV3Verifier;
import android.util.apk.ApkSignatureSchemeV4Verifier;
import android.util.apk.ApkSignatureVerifier;
import android.util.apk.ApkSigningBlockUtils;
import android.util.apk.ByteBufferFactory;
import android.util.apk.SignatureInfo;
import android.util.apk.SignatureNotFoundException;
import android.util.apk.VerityBuilder;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.security.VerityUtils;
import com.android.server.pm.pkg.AndroidPackage;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.security.DigestException;
import java.security.InvalidParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;
/**
* Provides checksums for APK.
*/
public class ApkChecksums {
static final String TAG = "ApkChecksums";
private static final String DIGESTS_FILE_EXTENSION = ".digests";
private static final String DIGESTS_SIGNATURE_FILE_EXTENSION = ".signature";
// MessageDigest algorithms.
static final String ALGO_MD5 = "MD5";
static final String ALGO_SHA1 = "SHA1";
static final String ALGO_SHA256 = "SHA256";
static final String ALGO_SHA512 = "SHA512";
private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = {};
/**
* Arbitrary size restriction for the signature, used to sign the checksums.
*/
private static final int MAX_SIGNATURE_SIZE_BYTES = 35 * 1024;
/**
* Check back in 1 second after we detected we needed to wait for the APK to be fully available.
*/
private static final long PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS = 1000;
/**
* 24 hours timeout to wait till all files are loaded.
*/
private static final long PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS = 1000 * 3600 * 24;
/**
* Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors.
*
* NOTE: All getters should return the same instance for every call.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static class Injector {
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
interface Producer<T> {
/** Produce an instance of type {@link T} */
T produce();
}
private final Producer<Context> mContext;
private final Producer<Handler> mHandlerProducer;
private final Producer<IncrementalManager> mIncrementalManagerProducer;
private final Producer<PackageManagerInternal> mPackageManagerInternalProducer;
Injector(Producer<Context> context, Producer<Handler> handlerProducer,
Producer<IncrementalManager> incrementalManagerProducer,
Producer<PackageManagerInternal> packageManagerInternalProducer) {
mContext = context;
mHandlerProducer = handlerProducer;
mIncrementalManagerProducer = incrementalManagerProducer;
mPackageManagerInternalProducer = packageManagerInternalProducer;
}
public Context getContext() {
return mContext.produce();
}
public Handler getHandler() {
return mHandlerProducer.produce();
}
public IncrementalManager getIncrementalManager() {
return mIncrementalManagerProducer.produce();
}
public PackageManagerInternal getPackageManagerInternal() {
return mPackageManagerInternalProducer.produce();
}
}
/**
* Return the digests path associated with the given code path
* (replaces '.apk' extension with '.digests')
*
* @throws IllegalArgumentException if the code path is not an .apk.
*/
public static String buildDigestsPathForApk(String codePath) {
if (!ApkLiteParseUtils.isApkPath(codePath)) {
throw new IllegalStateException("Code path is not an apk " + codePath);
}
return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length())
+ DIGESTS_FILE_EXTENSION;
}
/**
* Return the signature path associated with the given digests path.
* (appends '.signature' to the end)
*/
public static String buildSignaturePathForDigests(String digestsPath) {
return digestsPath + DIGESTS_SIGNATURE_FILE_EXTENSION;
}
/** Returns true if the given file looks like containing digests or digests' signature. */
public static boolean isDigestOrDigestSignatureFile(File file) {
final String name = file.getName();
return name.endsWith(DIGESTS_FILE_EXTENSION) || name.endsWith(
DIGESTS_SIGNATURE_FILE_EXTENSION);
}
/**
* Search for the digests file associated with the given target file.
* If it exists, the method returns the digests file; otherwise it returns null.
*/
public static File findDigestsForFile(File targetFile) {
String digestsPath = buildDigestsPathForApk(targetFile.getAbsolutePath());
File digestsFile = new File(digestsPath);
return digestsFile.exists() ? digestsFile : null;
}
/**
* Search for the signature file associated with the given digests file.
* If it exists, the method returns the signature file; otherwise it returns null.
*/
public static File findSignatureForDigests(File digestsFile) {
String signaturePath = buildSignaturePathForDigests(digestsFile.getAbsolutePath());
File signatureFile = new File(signaturePath);
return signatureFile.exists() ? signatureFile : null;
}
/**
* Serialize checksums to the stream in binary format.
*/
public static void writeChecksums(OutputStream os, Checksum[] checksums)
throws IOException {
try (DataOutputStream dos = new DataOutputStream(os)) {
for (Checksum checksum : checksums) {
Checksum.writeToStream(dos, checksum);
}
}
}
private static Checksum[] readChecksums(File file) throws IOException {
try (InputStream is = new FileInputStream(file)) {
return readChecksums(is);
}
}
/**
* Deserialize array of checksums previously stored in
* {@link #writeChecksums(OutputStream, Checksum[])}.
*/
public static Checksum[] readChecksums(InputStream is) throws IOException {
try (DataInputStream dis = new DataInputStream(is)) {
ArrayList<Checksum> checksums = new ArrayList<>();
try {
// 100 is an arbitrary very big number. We should stop at EOF.
for (int i = 0; i < 100; ++i) {
checksums.add(Checksum.readFromStream(dis));
}
} catch (EOFException e) {
// expected
}
return checksums.toArray(new Checksum[checksums.size()]);
}
}
/**
* Verifies signature over binary serialized checksums.
* @param checksums array of checksums
* @param signature detached PKCS7 signature in DER format
* @return all certificates that passed verification
* @throws SignatureException if verification fails
*/
public static @NonNull Certificate[] verifySignature(Checksum[] checksums, byte[] signature)
throws NoSuchAlgorithmException, IOException, SignatureException {
if (signature == null || signature.length > MAX_SIGNATURE_SIZE_BYTES) {
throw new SignatureException("Invalid signature");
}
final byte[] blob;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
writeChecksums(os, checksums);
blob = os.toByteArray();
}
PKCS7 pkcs7 = new PKCS7(signature);
final Certificate[] certs = pkcs7.getCertificates();
if (certs == null || certs.length == 0) {
throw new SignatureException("Signature missing certificates");
}
final SignerInfo[] signerInfos = pkcs7.verify(blob);
if (signerInfos == null || signerInfos.length == 0) {
throw new SignatureException("Verification failed");
}
ArrayList<Certificate> certificates = new ArrayList<>(signerInfos.length);
for (SignerInfo signerInfo : signerInfos) {
ArrayList<X509Certificate> chain = signerInfo.getCertificateChain(pkcs7);
if (chain == null) {
throw new SignatureException(
"Verification passed, but certification chain is empty.");
}
certificates.addAll(chain);
}
return certificates.toArray(new Certificate[certificates.size()]);
}
/**
* Fetch or calculate checksums for the collection of files.
*
* @param filesToChecksum split name, null for base and File to fetch checksums for
* @param optional mask to fetch readily available checksums
* @param required mask to forcefully calculate if not available
* @param installerPackageName package name of the installer of the packages
* @param trustedInstallers array of certificate to trust, two specific cases:
* null - trust anybody,
* [] - trust nobody.
* @param onChecksumsReadyListener to receive the resulting checksums
*/
public static void getChecksums(List<Pair<String, File>> filesToChecksum,
@Checksum.TypeMask int optional,
@Checksum.TypeMask int required,
@Nullable String installerPackageName,
@Nullable Certificate[] trustedInstallers,
@NonNull IOnChecksumsReadyListener onChecksumsReadyListener,
@NonNull Injector injector) {
List<Map<Integer, ApkChecksum>> result = new ArrayList<>(filesToChecksum.size());
for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
final String split = filesToChecksum.get(i).first;
final File file = filesToChecksum.get(i).second;
Map<Integer, ApkChecksum> checksums = new ArrayMap<>();
result.add(checksums);
try {
getAvailableApkChecksums(split, file, optional | required, installerPackageName,
trustedInstallers, checksums, injector);
} catch (Throwable e) {
Slog.e(TAG, "Preferred checksum calculation error", e);
}
}
long startTime = SystemClock.uptimeMillis();
processRequiredChecksums(filesToChecksum, result, required, onChecksumsReadyListener,
injector, startTime);
}
private static void processRequiredChecksums(List<Pair<String, File>> filesToChecksum,
List<Map<Integer, ApkChecksum>> result,
@Checksum.TypeMask int required,
@NonNull IOnChecksumsReadyListener onChecksumsReadyListener,
@NonNull Injector injector,
long startTime) {
final boolean timeout =
SystemClock.uptimeMillis() - startTime >= PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS;
List<ApkChecksum> allChecksums = new ArrayList<>();
for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
final String split = filesToChecksum.get(i).first;
final File file = filesToChecksum.get(i).second;
Map<Integer, ApkChecksum> checksums = result.get(i);
try {
if (!timeout || required != 0) {
if (needToWait(file, required, checksums, injector)) {
// Not ready, come back later.
injector.getHandler().postDelayed(() -> {
processRequiredChecksums(filesToChecksum, result, required,
onChecksumsReadyListener, injector, startTime);
}, PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS);
return;
}
getRequiredApkChecksums(split, file, required, checksums);
}
allChecksums.addAll(checksums.values());
} catch (Throwable e) {
Slog.e(TAG, "Required checksum calculation error", e);
}
}
try {
onChecksumsReadyListener.onChecksumsReady(allChecksums);
} catch (RemoteException e) {
Slog.w(TAG, e);
}
}
/**
* Fetch readily available checksums - enforced by kernel or provided by Installer.
*
* @param split split name, null for base
* @param file to fetch checksums for
* @param types mask to fetch checksums
* @param installerPackageName package name of the installer of the packages
* @param trustedInstallers array of certificate to trust, two specific cases:
* null - trust anybody,
* [] - trust nobody.
* @param checksums resulting checksums
*/
private static void getAvailableApkChecksums(String split, File file,
@Checksum.TypeMask int types,
@Nullable String installerPackageName,
@Nullable Certificate[] trustedInstallers,
Map<Integer, ApkChecksum> checksums,
@NonNull Injector injector) {
if (!file.exists()) {
return;
}
final String filePath = file.getAbsolutePath();
// Always available: FSI or IncFs.
if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) {
// Hashes in fs-verity and IncFS are always verified.
ApkChecksum checksum = extractHashFromFS(split, filePath);
if (checksum != null) {
checksums.put(checksum.getType(), checksum);
}
}
// System enforced: v2/v3.
if (isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums) || isRequired(
TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) {
Map<Integer, ApkChecksum> v2v3checksums = extractHashFromV2V3Signature(
split, filePath, types);
if (v2v3checksums != null) {
checksums.putAll(v2v3checksums);
}
}
// Note: this compares installer and system digests internally and
// has to be called right after all system digests are populated.
getInstallerChecksums(split, file, types, installerPackageName, trustedInstallers,
checksums, injector);
}
private static void getInstallerChecksums(String split, File file,
@Checksum.TypeMask int types,
@Nullable String installerPackageName,
@Nullable Certificate[] trustedInstallers,
Map<Integer, ApkChecksum> checksums,
@NonNull Injector injector) {
if (PackageManagerServiceUtils.isInstalledByAdb(installerPackageName)) {
return;
}
if (trustedInstallers != null && trustedInstallers.length == 0) {
return;
}
final File digestsFile = findDigestsForFile(file);
if (digestsFile == null) {
return;
}
final File signatureFile = findSignatureForDigests(digestsFile);
try {
final Checksum[] digests = readChecksums(digestsFile);
final Signature[] certs;
final Signature[] pastCerts;
if (signatureFile != null) {
final Certificate[] certificates = verifySignature(digests,
Files.readAllBytes(signatureFile.toPath()));
if (certificates == null || certificates.length == 0) {
Slog.e(TAG, "Error validating signature");
return;
}
certs = new Signature[certificates.length];
for (int i = 0, size = certificates.length; i < size; i++) {
certs[i] = new Signature(certificates[i].getEncoded());
}
pastCerts = null;
} else {
final AndroidPackage installer = injector.getPackageManagerInternal().getPackage(
installerPackageName);
if (installer == null) {
Slog.e(TAG, "Installer package not found.");
return;
}
// Obtaining array of certificates used for signing the installer package.
certs = installer.getSigningDetails().getSignatures();
pastCerts = installer.getSigningDetails().getPastSigningCertificates();
}
if (certs == null || certs.length == 0 || certs[0] == null) {
Slog.e(TAG, "Can't obtain certificates.");
return;
}
// According to V2/V3 signing schema, the first certificate corresponds to the public
// key in the signing block.
byte[] trustedCertBytes = certs[0].toByteArray();
final Set<Signature> trusted = convertToSet(trustedInstallers);
if (trusted != null && !trusted.isEmpty()) {
// Obtaining array of certificates used for signing the installer package.
Signature trustedCert = isTrusted(certs, trusted);
if (trustedCert == null) {
trustedCert = isTrusted(pastCerts, trusted);
}
if (trustedCert == null) {
return;
}
trustedCertBytes = trustedCert.toByteArray();
}
// Compare OS-enforced digests.
for (Checksum digest : digests) {
final ApkChecksum system = checksums.get(digest.getType());
if (system != null && !Arrays.equals(system.getValue(), digest.getValue())) {
throw new InvalidParameterException("System digest " + digest.getType()
+ " mismatch, can't bind installer-provided digests to the APK.");
}
}
// Append missing digests.
for (Checksum digest : digests) {
if (isRequired(digest.getType(), types, checksums)) {
checksums.put(digest.getType(),
new ApkChecksum(split, digest, installerPackageName, trustedCertBytes));
}
}
} catch (IOException e) {
Slog.e(TAG, "Error reading .digests or .signature", e);
} catch (NoSuchAlgorithmException | SignatureException | InvalidParameterException e) {
Slog.e(TAG, "Error validating digests. Invalid digests will be removed", e);
try {
Files.deleteIfExists(digestsFile.toPath());
if (signatureFile != null) {
Files.deleteIfExists(signatureFile.toPath());
}
} catch (IOException ignored) {
}
} catch (CertificateEncodingException e) {
Slog.e(TAG, "Error encoding trustedInstallers", e);
}
}
/**
* Whether the file is available for checksumming or we need to wait.
*/
private static boolean needToWait(File file,
@Checksum.TypeMask int types,
Map<Integer, ApkChecksum> checksums,
@NonNull Injector injector) throws IOException {
if (!isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)
&& !isRequired(TYPE_WHOLE_MD5, types, checksums)
&& !isRequired(TYPE_WHOLE_SHA1, types, checksums)
&& !isRequired(TYPE_WHOLE_SHA256, types, checksums)
&& !isRequired(TYPE_WHOLE_SHA512, types, checksums)
&& !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums)
&& !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) {
return false;
}
final String filePath = file.getAbsolutePath();
if (!IncrementalManager.isIncrementalPath(filePath)) {
return false;
}
IncrementalManager manager = injector.getIncrementalManager();
if (manager == null) {
Slog.e(TAG, "IncrementalManager is missing.");
return false;
}
IncrementalStorage storage = manager.openStorage(filePath);
if (storage == null) {
Slog.e(TAG, "IncrementalStorage is missing for a path on IncFs: " + filePath);
return false;
}
return !storage.isFileFullyLoaded(filePath);
}
/**
* Fetch or calculate checksums for the specific file.
*
* @param split split name, null for base
* @param file to fetch checksums for
* @param types mask to forcefully calculate if not available
* @param checksums resulting checksums
*/
private static void getRequiredApkChecksums(String split, File file,
@Checksum.TypeMask int types,
Map<Integer, ApkChecksum> checksums) {
final String filePath = file.getAbsolutePath();
// Manually calculating required checksums if not readily available.
if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) {
try {
byte[] generatedRootHash = VerityBuilder.generateFsVerityRootHash(
filePath, /*salt=*/null,
new ByteBufferFactory() {
@Override
public ByteBuffer create(int capacity) {
return ByteBuffer.allocate(capacity);
}
});
checksums.put(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256,
new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256,
verityHashForFile(file, generatedRootHash)));
} catch (IOException | NoSuchAlgorithmException | DigestException e) {
Slog.e(TAG, "Error calculating WHOLE_MERKLE_ROOT_4K_SHA256", e);
}
}
calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_MD5);
calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA1);
calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA256);
calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA512);
calculatePartialChecksumsIfRequested(checksums, split, file, types);
}
private static boolean isRequired(@Checksum.Type int type,
@Checksum.TypeMask int types, Map<Integer, ApkChecksum> checksums) {
if ((types & type) == 0) {
return false;
}
if (checksums.containsKey(type)) {
return false;
}
return true;
}
/**
* Signature class provides a fast way to compare certificates using their hashes.
* The hash is exactly the same as in X509/Certificate.
*/
private static Set<Signature> convertToSet(@Nullable Certificate[] array) throws
CertificateEncodingException {
if (array == null) {
return null;
}
final Set<Signature> set = new ArraySet<>(array.length);
for (Certificate item : array) {
set.add(new Signature(item.getEncoded()));
}
return set;
}
private static Signature isTrusted(Signature[] signatures, Set<Signature> trusted) {
if (signatures == null) {
return null;
}
for (Signature signature : signatures) {
if (trusted.contains(signature)) {
return signature;
}
}
return null;
}
private static ApkChecksum extractHashFromFS(String split, String filePath) {
// verity first
if (VerityUtils.hasFsverity(filePath)) {
byte[] verityHash = VerityUtils.getFsverityDigest(filePath);
if (verityHash != null) {
return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, verityHash);
}
}
// v4 next
try {
ApkSignatureSchemeV4Verifier.VerifiedSigner signer =
ApkSignatureSchemeV4Verifier.extractCertificates(filePath);
byte[] rootHash = signer.contentDigests.getOrDefault(
CONTENT_DIGEST_VERITY_CHUNKED_SHA256, null);
if (rootHash != null) {
return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256,
verityHashForFile(new File(filePath), rootHash));
}
} catch (SignatureNotFoundException e) {
// Nothing
} catch (SignatureException | SecurityException e) {
Slog.e(TAG, "V4 signature error", e);
}
return null;
}
/**
* Returns fs-verity digest as described in
* https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#fs-verity-descriptor
* @param file the Merkle tree is built over
* @param rootHash Merkle tree root hash
*/
static byte[] verityHashForFile(File file, byte[] rootHash) {
try {
ByteBuffer buffer = ByteBuffer.allocate(256); // sizeof(fsverity_descriptor)
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put((byte) 1); // __u8 version, must be 1
buffer.put((byte) 1); // __u8 hash_algorithm, FS_VERITY_HASH_ALG_SHA256
buffer.put((byte) 12); // __u8, FS_VERITY_LOG_BLOCKSIZE
buffer.put((byte) 0); // __u8, size of salt in bytes; 0 if none
buffer.putInt(0); // __le32 __reserved_0x04, must be 0
buffer.putLong(file.length()); // __le64 data_size
buffer.put(rootHash); // root_hash, first 32 bytes
final int padding = 32 + 32 + 144; // root_hash, last 32 bytes, we are using sha256.
// salt, 32 bytes
// reserved, 144 bytes
for (int i = 0; i < padding; ++i) {
buffer.put((byte) 0);
}
buffer.flip();
final MessageDigest md = MessageDigest.getInstance(ALGO_SHA256);
md.update(buffer);
return md.digest();
} catch (NoSuchAlgorithmException e) {
Slog.e(TAG, "Device does not support MessageDigest algorithm", e);
return null;
}
}
private static Map<Integer, ApkChecksum> extractHashFromV2V3Signature(
String split, String filePath, int types) {
Map<Integer, byte[]> contentDigests = null;
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> result =
ApkSignatureVerifier.verifySignaturesInternal(input, filePath,
SignatureSchemeVersion.SIGNING_BLOCK_V2, false /*verifyFull*/);
if (result.isError()) {
if (!(result.getException() instanceof SignatureNotFoundException)) {
Slog.e(TAG, "Signature verification error", result.getException());
}
} else {
contentDigests = result.getResult().contentDigests;
}
if (contentDigests == null) {
return null;
}
Map<Integer, ApkChecksum> checksums = new ArrayMap<>();
if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0) {
byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA256, null);
if (hash != null) {
checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256,
new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, hash));
}
}
if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0) {
byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA512, null);
if (hash != null) {
checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512,
new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, hash));
}
}
return checksums;
}
private static String getMessageDigestAlgoForChecksumKind(int type)
throws NoSuchAlgorithmException {
switch (type) {
case TYPE_WHOLE_MD5:
return ALGO_MD5;
case TYPE_WHOLE_SHA1:
return ALGO_SHA1;
case TYPE_WHOLE_SHA256:
return ALGO_SHA256;
case TYPE_WHOLE_SHA512:
return ALGO_SHA512;
default:
throw new NoSuchAlgorithmException("Invalid checksum type: " + type);
}
}
private static void calculateChecksumIfRequested(Map<Integer, ApkChecksum> checksums,
String split, File file, int required, int type) {
if ((required & type) != 0 && !checksums.containsKey(type)) {
final byte[] checksum = getApkChecksum(file, type);
if (checksum != null) {
checksums.put(type, new ApkChecksum(split, type, checksum));
}
}
}
static final int MIN_BUFFER_SIZE = 4 * 1024;
static final int MAX_BUFFER_SIZE = 128 * 1024;
private static byte[] getApkChecksum(File file, int type) {
final int bufferSize = (int) Math.max(MIN_BUFFER_SIZE,
Math.min(MAX_BUFFER_SIZE, file.length()));
try (FileInputStream fis = new FileInputStream(file)) {
final byte[] buffer = new byte[bufferSize];
int nread = 0;
final String algo = getMessageDigestAlgoForChecksumKind(type);
MessageDigest md = MessageDigest.getInstance(algo);
while ((nread = fis.read(buffer)) != -1) {
md.update(buffer, 0, nread);
}
return md.digest();
} catch (IOException e) {
Slog.e(TAG, "Error reading " + file.getAbsolutePath() + " to compute hash.", e);
return null;
} catch (NoSuchAlgorithmException e) {
Slog.e(TAG, "Device does not support MessageDigest algorithm", e);
return null;
}
}
private static int[] getContentDigestAlgos(boolean needSignatureSha256,
boolean needSignatureSha512) {
if (needSignatureSha256 && needSignatureSha512) {
// Signature block present, but no digests???
return new int[]{CONTENT_DIGEST_CHUNKED_SHA256, CONTENT_DIGEST_CHUNKED_SHA512};
} else if (needSignatureSha256) {
return new int[]{CONTENT_DIGEST_CHUNKED_SHA256};
} else {
return new int[]{CONTENT_DIGEST_CHUNKED_SHA512};
}
}
private static int getChecksumKindForContentDigestAlgo(int contentDigestAlgo) {
switch (contentDigestAlgo) {
case CONTENT_DIGEST_CHUNKED_SHA256:
return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256;
case CONTENT_DIGEST_CHUNKED_SHA512:
return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
default:
return -1;
}
}
private static void calculatePartialChecksumsIfRequested(Map<Integer, ApkChecksum> checksums,
String split, File file, int required) {
boolean needSignatureSha256 =
(required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0 && !checksums.containsKey(
TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
boolean needSignatureSha512 =
(required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0 && !checksums.containsKey(
TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
if (!needSignatureSha256 && !needSignatureSha512) {
return;
}
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
SignatureInfo signatureInfo = null;
try {
signatureInfo = ApkSignatureSchemeV3Verifier.findSignature(raf);
} catch (SignatureNotFoundException e) {
try {
signatureInfo = ApkSignatureSchemeV2Verifier.findSignature(raf);
} catch (SignatureNotFoundException ee) {
}
}
if (signatureInfo == null) {
Slog.e(TAG, "V2/V3 signatures not found in " + file.getAbsolutePath());
return;
}
final int[] digestAlgos = getContentDigestAlgos(needSignatureSha256,
needSignatureSha512);
byte[][] digests = ApkSigningBlockUtils.computeContentDigestsPer1MbChunk(digestAlgos,
raf.getFD(), signatureInfo);
for (int i = 0, size = digestAlgos.length; i < size; ++i) {
int checksumKind = getChecksumKindForContentDigestAlgo(digestAlgos[i]);
if (checksumKind != -1) {
checksums.put(checksumKind, new ApkChecksum(split, checksumKind, digests[i]));
}
}
} catch (IOException | DigestException e) {
Slog.e(TAG, "Error computing hash.", e);
}
}
}