| /* |
| * Copyright (C) 2022 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; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.apex.ApexInfo; |
| import android.apex.IApexService; |
| import android.app.compat.CompatChanges; |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.app.job.JobScheduler; |
| import android.app.job.JobService; |
| import android.compat.annotation.ChangeId; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApexStagedEvent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IBackgroundInstallControlService; |
| import android.content.pm.IPackageManagerNative; |
| import android.content.pm.IStagedApexObserver; |
| import android.content.pm.InstallSourceInfo; |
| import android.content.pm.ModuleInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.ParceledListSlice; |
| import android.content.pm.SharedLibraryInfo; |
| import android.content.pm.Signature; |
| import android.content.pm.SigningDetails; |
| import android.content.pm.SigningInfo; |
| import android.content.pm.parsing.result.ParseInput; |
| import android.content.pm.parsing.result.ParseResult; |
| import android.content.pm.parsing.result.ParseTypeImpl; |
| import android.hardware.biometrics.SensorProperties; |
| import android.hardware.biometrics.SensorProperties.ComponentInfo; |
| import android.hardware.face.FaceManager; |
| import android.hardware.face.FaceSensorProperties; |
| import android.hardware.face.FaceSensorPropertiesInternal; |
| import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; |
| import android.hardware.fingerprint.FingerprintManager; |
| import android.hardware.fingerprint.FingerprintSensorProperties; |
| import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; |
| import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.os.ServiceManager; |
| import android.os.ShellCallback; |
| import android.os.ShellCommand; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.provider.DeviceConfig; |
| import android.text.TextUtils; |
| import android.util.PackageUtils; |
| import android.util.Slog; |
| import android.util.apk.ApkSignatureVerifier; |
| import android.util.apk.ApkSigningBlockUtils; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.modules.expresslog.Histogram; |
| import com.android.internal.os.IBinaryTransparencyService; |
| import com.android.internal.util.FrameworkStatsLog; |
| import com.android.server.pm.ApexManager; |
| import com.android.server.pm.pkg.AndroidPackage; |
| import com.android.server.pm.pkg.AndroidPackageSplit; |
| import com.android.server.pm.pkg.PackageState; |
| |
| import libcore.util.HexEncoding; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.security.PublicKey; |
| import java.security.cert.CertificateException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Executors; |
| import java.util.stream.Collectors; |
| |
| /** |
| * @hide |
| */ |
| public class BinaryTransparencyService extends SystemService { |
| private static final String TAG = "TransparencyService"; |
| |
| @VisibleForTesting |
| static final String VBMETA_DIGEST_UNINITIALIZED = "vbmeta-digest-uninitialized"; |
| @VisibleForTesting |
| static final String VBMETA_DIGEST_UNAVAILABLE = "vbmeta-digest-unavailable"; |
| @VisibleForTesting |
| static final String SYSPROP_NAME_VBETA_DIGEST = "ro.boot.vbmeta.digest"; |
| |
| @VisibleForTesting |
| static final String BINARY_HASH_ERROR = "SHA256HashError"; |
| |
| static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000; |
| |
| static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined"; |
| |
| // Copy from the atom. Consistent for both ApexInfoGathered and MobileBundledAppInfoGathered. |
| static final int DIGEST_ALGORITHM_UNKNOWN = 0; |
| static final int DIGEST_ALGORITHM_CHUNKED_SHA256 = 1; |
| static final int DIGEST_ALGORITHM_CHUNKED_SHA512 = 2; |
| static final int DIGEST_ALGORITHM_VERITY_CHUNKED_SHA256 = 3; |
| static final int DIGEST_ALGORITHM_SHA256 = 4; |
| |
| // used for indicating any type of error during MBA measurement |
| static final int MBA_STATUS_ERROR = 0; |
| // used for indicating factory condition preloads |
| static final int MBA_STATUS_PRELOADED = 1; |
| // used for indicating preloaded apps that are updated |
| static final int MBA_STATUS_UPDATED_PRELOAD = 2; |
| // used for indicating newly installed MBAs |
| static final int MBA_STATUS_NEW_INSTALL = 3; |
| // used for indicating newly installed MBAs that are updated (but unused currently) |
| static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4; |
| |
| @VisibleForTesting |
| static final String KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION = |
| "enable_biometric_property_verification"; |
| |
| private static final boolean DEBUG = false; // toggle this for local debug |
| |
| private static final Histogram digestAllPackagesLatency = new Histogram( |
| "binary_transparency.value_digest_all_packages_latency_uniform", |
| new Histogram.UniformOptions(50, 0, 500)); |
| |
| private final Context mContext; |
| private String mVbmetaDigest; |
| // the system time (in ms) the last measurement was taken |
| private long mMeasurementsLastRecordedMs; |
| private PackageManagerInternal mPackageManagerInternal; |
| private BiometricLogger mBiometricLogger; |
| |
| /** |
| * Guards whether or not measurements of MBA to be performed. When this change is enabled, |
| * measurements of MBAs are performed. But when it is disabled, only measurements of APEX |
| * and modules are done. |
| */ |
| @ChangeId |
| public static final long LOG_MBA_INFO = 245692487L; |
| |
| final class BinaryTransparencyServiceImpl extends IBinaryTransparencyService.Stub { |
| |
| @Override |
| public String getSignedImageInfo() { |
| return mVbmetaDigest; |
| } |
| |
| /** |
| * A helper function to compute the SHA256 digest of APK package signer. |
| * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}. |
| * @return an array of {@code String} representing hex encoded string of the |
| * SHA256 digest of APK signer(s). The number of signers will be reflected by the |
| * size of the array. |
| * However, {@code null} is returned if there is any error. |
| */ |
| private String[] computePackageSignerSha256Digests(@Nullable SigningInfo signingInfo) { |
| if (signingInfo == null) { |
| Slog.e(TAG, "signingInfo is null"); |
| return null; |
| } |
| |
| Signature[] packageSigners = signingInfo.getApkContentsSigners(); |
| List<String> resultList = new ArrayList<>(); |
| for (Signature packageSigner : packageSigners) { |
| byte[] digest = PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray()); |
| String digestHexString = HexEncoding.encodeToString(digest, false); |
| resultList.add(digestHexString); |
| } |
| return resultList.toArray(new String[1]); |
| } |
| |
| /* |
| * Perform basic measurement (i.e. content digest) on a given app, including the split APKs. |
| * |
| * @param packageState The package to be measured. |
| * @param mbaStatus Assign this value of MBA status to the returned elements. |
| * @return a @{@code List<IBinaryTransparencyService.AppInfo>} |
| */ |
| private @NonNull List<IBinaryTransparencyService.AppInfo> collectAppInfo( |
| PackageState packageState, int mbaStatus) { |
| // compute content digest |
| if (DEBUG) { |
| Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at " |
| + packageState.getPath()); |
| } |
| |
| var results = new ArrayList<IBinaryTransparencyService.AppInfo>(); |
| |
| // Same attributes across base and splits. |
| String packageName = packageState.getPackageName(); |
| long versionCode = packageState.getVersionCode(); |
| String[] signerDigests = |
| computePackageSignerSha256Digests(packageState.getSigningInfo()); |
| |
| AndroidPackage pkg = packageState.getAndroidPackage(); |
| for (AndroidPackageSplit split : pkg.getSplits()) { |
| var appInfo = new IBinaryTransparencyService.AppInfo(); |
| appInfo.packageName = packageName; |
| appInfo.longVersion = versionCode; |
| appInfo.splitName = split.getName(); // base's split name is null |
| // Signer digests are consistent between splits, guaranteed by Package Manager. |
| appInfo.signerDigests = signerDigests; |
| appInfo.mbaStatus = mbaStatus; |
| |
| // Only digest and split name are different between splits. |
| Digest digest = measureApk(split.getPath()); |
| appInfo.digest = digest.value; |
| appInfo.digestAlgorithm = digest.algorithm; |
| |
| results.add(appInfo); |
| } |
| |
| // InstallSourceInfo is only available per package name, so store it only on the base |
| // APK. It's not current currently available in PackageState (there's a TODO), to we |
| // need to extract manually with another call. |
| // |
| // Base APK is already the 0-th split from getSplits() and can't be null. |
| AppInfo base = results.get(0); |
| InstallSourceInfo installSourceInfo = getInstallSourceInfo( |
| packageState.getPackageName()); |
| if (installSourceInfo != null) { |
| base.initiator = installSourceInfo.getInitiatingPackageName(); |
| SigningInfo initiatorSignerInfo = |
| installSourceInfo.getInitiatingPackageSigningInfo(); |
| if (initiatorSignerInfo != null) { |
| base.initiatorSignerDigests = |
| computePackageSignerSha256Digests(initiatorSignerInfo); |
| } |
| base.installer = installSourceInfo.getInstallingPackageName(); |
| base.originator = installSourceInfo.getOriginatingPackageName(); |
| } |
| |
| return results; |
| } |
| |
| /** |
| * Perform basic measurement (i.e. content digest) on a given APK. |
| * |
| * @param apkPath The APK (or APEX, since it's also an APK) file to be measured. |
| * @return a {@link #Digest} with preferred digest algorithm type and the value. |
| */ |
| private @Nullable Digest measureApk(@NonNull String apkPath) { |
| // compute content digest |
| Map<Integer, byte[]> contentDigests = computeApkContentDigest(apkPath); |
| if (contentDigests == null) { |
| Slog.d(TAG, "Failed to compute content digest for " + apkPath); |
| } else { |
| // in this iteration, we'll be supporting only 2 types of digests: |
| // CHUNKED_SHA256 and CHUNKED_SHA512. |
| // And only one of them will be available per package. |
| if (contentDigests.containsKey( |
| ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) { |
| return new Digest( |
| DIGEST_ALGORITHM_CHUNKED_SHA256, |
| contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)); |
| } else if (contentDigests.containsKey( |
| ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) { |
| return new Digest( |
| DIGEST_ALGORITHM_CHUNKED_SHA512, |
| contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)); |
| } |
| } |
| // When something went wrong, fall back to simple sha256. |
| byte[] digest = PackageUtils.computeSha256DigestForLargeFileAsBytes(apkPath, |
| PackageUtils.createLargeFileBuffer()); |
| return new Digest(DIGEST_ALGORITHM_SHA256, digest); |
| } |
| |
| |
| /** |
| * Measures and records digests for *all* covered binaries/packages. |
| * |
| * This method will be called in a Job scheduled to take measurements periodically. If the |
| * last measurement was performaned recently (less than RECORD_MEASUREMENT_COOLDOWN_MS |
| * ago), the measurement and recording will be skipped. |
| * |
| * Packages that are covered so far are: |
| * - all APEXs (introduced in Android T) |
| * - all mainline modules (introduced in Android T) |
| * - all preloaded apps and their update(s) (new in Android U) |
| * - dynamically installed mobile bundled apps (MBAs) (new in Android U) |
| */ |
| public void recordMeasurementsForAllPackages() { |
| // check if we should measure and record |
| long currentTimeMs = System.currentTimeMillis(); |
| if ((currentTimeMs - mMeasurementsLastRecordedMs) < RECORD_MEASUREMENTS_COOLDOWN_MS) { |
| Slog.d(TAG, "Skip measurement since the last measurement was only taken at " |
| + mMeasurementsLastRecordedMs + " within the cooldown period"); |
| return; |
| } |
| Slog.d(TAG, "Measurement was last taken at " + mMeasurementsLastRecordedMs |
| + " and is now updated to: " + currentTimeMs); |
| mMeasurementsLastRecordedMs = currentTimeMs; |
| |
| Bundle packagesMeasured = new Bundle(); |
| |
| // measure all APEXs first |
| if (DEBUG) { |
| Slog.d(TAG, "Measuring APEXs..."); |
| } |
| List<IBinaryTransparencyService.ApexInfo> allApexInfo = collectAllApexInfo( |
| /* includeTestOnly */ false); |
| for (IBinaryTransparencyService.ApexInfo apexInfo : allApexInfo) { |
| packagesMeasured.putBoolean(apexInfo.packageName, true); |
| |
| recordApexInfo(apexInfo); |
| } |
| if (DEBUG) { |
| Slog.d(TAG, "Measured " + packagesMeasured.size() |
| + " packages after considering APEXs."); |
| } |
| |
| // proceed with all preloaded apps |
| List<IBinaryTransparencyService.AppInfo> allUpdatedPreloadInfo = |
| collectAllUpdatedPreloadInfo(packagesMeasured); |
| for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) { |
| packagesMeasured.putBoolean(appInfo.packageName, true); |
| writeAppInfoToLog(appInfo); |
| } |
| if (DEBUG) { |
| Slog.d(TAG, "Measured " + packagesMeasured.size() |
| + " packages after considering preloads"); |
| } |
| |
| if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) { |
| // lastly measure all newly installed MBAs |
| List<IBinaryTransparencyService.AppInfo> allMbaInfo = |
| collectAllSilentInstalledMbaInfo(packagesMeasured); |
| for (IBinaryTransparencyService.AppInfo appInfo : allMbaInfo) { |
| packagesMeasured.putBoolean(appInfo.packageName, true); |
| writeAppInfoToLog(appInfo); |
| } |
| } |
| long timeSpentMeasuring = System.currentTimeMillis() - currentTimeMs; |
| digestAllPackagesLatency.logSample(timeSpentMeasuring); |
| if (DEBUG) { |
| Slog.d(TAG, "Measured " + packagesMeasured.size() |
| + " packages altogether in " + timeSpentMeasuring + "ms"); |
| } |
| } |
| |
| @Override |
| public List<IBinaryTransparencyService.ApexInfo> collectAllApexInfo( |
| boolean includeTestOnly) { |
| var results = new ArrayList<IBinaryTransparencyService.ApexInfo>(); |
| for (PackageInfo packageInfo : getCurrentInstalledApexs()) { |
| PackageState packageState = mPackageManagerInternal.getPackageStateInternal( |
| packageInfo.packageName); |
| if (packageState == null) { |
| Slog.w(TAG, "Package state is unavailable, ignoring the APEX " |
| + packageInfo.packageName); |
| continue; |
| } |
| |
| AndroidPackage pkg = packageState.getAndroidPackage(); |
| if (pkg == null) { |
| Slog.w(TAG, "Skipping the missing APK in " + pkg.getPath()); |
| continue; |
| } |
| Digest apexChecksum = measureApk(pkg.getPath()); |
| if (apexChecksum == null) { |
| Slog.w(TAG, "Skipping the missing APEX in " + pkg.getPath()); |
| continue; |
| } |
| |
| var apexInfo = new IBinaryTransparencyService.ApexInfo(); |
| apexInfo.packageName = packageState.getPackageName(); |
| apexInfo.longVersion = packageState.getVersionCode(); |
| apexInfo.digest = apexChecksum.value; |
| apexInfo.digestAlgorithm = apexChecksum.algorithm; |
| apexInfo.signerDigests = |
| computePackageSignerSha256Digests(packageState.getSigningInfo()); |
| |
| if (includeTestOnly) { |
| apexInfo.moduleName = apexPackageNameToModuleName( |
| packageState.getPackageName()); |
| } |
| |
| results.add(apexInfo); |
| } |
| return results; |
| } |
| |
| @Override |
| public List<IBinaryTransparencyService.AppInfo> collectAllUpdatedPreloadInfo( |
| Bundle packagesToSkip) { |
| final var results = new ArrayList<IBinaryTransparencyService.AppInfo>(); |
| |
| PackageManager pm = mContext.getPackageManager(); |
| mPackageManagerInternal.forEachPackageState((packageState) -> { |
| if (!packageState.isUpdatedSystemApp()) { |
| return; |
| } |
| if (packagesToSkip.containsKey(packageState.getPackageName())) { |
| return; |
| } |
| |
| Slog.d(TAG, "Preload " + packageState.getPackageName() + " at " |
| + packageState.getPath() + " has likely been updated."); |
| |
| List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo( |
| packageState, MBA_STATUS_UPDATED_PRELOAD); |
| results.addAll(resultsForApp); |
| }); |
| return results; |
| } |
| |
| @Override |
| public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo( |
| Bundle packagesToSkip) { |
| var results = new ArrayList<IBinaryTransparencyService.AppInfo>(); |
| for (PackageInfo packageInfo : getNewlyInstalledMbas()) { |
| if (packagesToSkip.containsKey(packageInfo.packageName)) { |
| continue; |
| } |
| PackageState packageState = mPackageManagerInternal.getPackageStateInternal( |
| packageInfo.packageName); |
| if (packageState == null) { |
| Slog.w(TAG, "Package state is unavailable, ignoring the package " |
| + packageInfo.packageName); |
| continue; |
| } |
| |
| List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo( |
| packageState, MBA_STATUS_NEW_INSTALL); |
| results.addAll(resultsForApp); |
| } |
| return results; |
| } |
| |
| private void recordApexInfo(IBinaryTransparencyService.ApexInfo apexInfo) { |
| // Must order by the proto's field number. |
| FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, |
| apexInfo.packageName, |
| apexInfo.longVersion, |
| (apexInfo.digest != null) ? HexEncoding.encodeToString(apexInfo.digest, false) |
| : null, |
| apexInfo.digestAlgorithm, |
| apexInfo.signerDigests); |
| } |
| |
| private void writeAppInfoToLog(IBinaryTransparencyService.AppInfo appInfo) { |
| // Must order by the proto's field number. |
| FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, |
| appInfo.packageName, |
| appInfo.longVersion, |
| (appInfo.digest != null) ? HexEncoding.encodeToString(appInfo.digest, false) |
| : null, |
| appInfo.digestAlgorithm, |
| appInfo.signerDigests, |
| appInfo.mbaStatus, |
| appInfo.initiator, |
| appInfo.initiatorSignerDigests, |
| appInfo.installer, |
| appInfo.originator, |
| appInfo.splitName); |
| } |
| |
| /** |
| * A wrapper around |
| * {@link ApkSignatureVerifier#verifySignaturesInternal(ParseInput, String, int, boolean)}. |
| * @param pathToApk The APK's installation path |
| * @return a {@code Map<Integer, byte[]>} with algorithm type as the key and content |
| * digest as the value. |
| * a {@code null} is returned upon encountering any error. |
| */ |
| private Map<Integer, byte[]> computeApkContentDigest(String pathToApk) { |
| final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); |
| ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> parseResult = |
| ApkSignatureVerifier.verifySignaturesInternal(input, |
| pathToApk, |
| SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2, false); |
| if (parseResult.isError()) { |
| Slog.e(TAG, "Failed to compute content digest for " |
| + pathToApk + " due to: " |
| + parseResult.getErrorMessage()); |
| return null; |
| } |
| return parseResult.getResult().contentDigests; |
| } |
| |
| @Override |
| public void onShellCommand(@Nullable FileDescriptor in, |
| @Nullable FileDescriptor out, |
| @Nullable FileDescriptor err, |
| @NonNull String[] args, |
| @Nullable ShellCallback callback, |
| @NonNull ResultReceiver resultReceiver) throws RemoteException { |
| (new ShellCommand() { |
| |
| private int printSignedImageInfo() { |
| final PrintWriter pw = getOutPrintWriter(); |
| boolean listAllPartitions = false; |
| String opt; |
| |
| while ((opt = getNextOption()) != null) { |
| switch (opt) { |
| case "-a": |
| listAllPartitions = true; |
| break; |
| default: |
| pw.println("ERROR: Unknown option: " + opt); |
| return 1; |
| } |
| } |
| |
| final String signedImageInfo = getSignedImageInfo(); |
| pw.println("Image Info:"); |
| pw.println(Build.FINGERPRINT); |
| pw.println(signedImageInfo); |
| pw.println(""); |
| |
| if (listAllPartitions) { |
| PackageManager pm = mContext.getPackageManager(); |
| if (pm == null) { |
| pw.println("ERROR: Failed to obtain an instance of package manager."); |
| return -1; |
| } |
| |
| pw.println("Other partitions:"); |
| List<Build.Partition> buildPartitions = Build.getFingerprintedPartitions(); |
| for (Build.Partition buildPartition : buildPartitions) { |
| pw.println("Name: " + buildPartition.getName()); |
| pw.println("Fingerprint: " + buildPartition.getFingerprint()); |
| pw.println("Build time (ms): " + buildPartition.getBuildTimeMillis()); |
| } |
| } |
| return 0; |
| } |
| |
| private void printPackageMeasurements(PackageInfo packageInfo, |
| boolean useSha256, |
| final PrintWriter pw) { |
| Map<Integer, byte[]> contentDigests = computeApkContentDigest( |
| packageInfo.applicationInfo.sourceDir); |
| if (contentDigests == null) { |
| pw.println("ERROR: Failed to compute package content digest for " |
| + packageInfo.applicationInfo.sourceDir); |
| return; |
| } |
| |
| if (useSha256) { |
| byte[] fileBuff = PackageUtils.createLargeFileBuffer(); |
| String hexEncodedSha256Digest = |
| PackageUtils.computeSha256DigestForLargeFile( |
| packageInfo.applicationInfo.sourceDir, fileBuff); |
| pw.print(hexEncodedSha256Digest + ","); |
| } |
| |
| for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { |
| Integer algorithmId = entry.getKey(); |
| byte[] contentDigest = entry.getValue(); |
| |
| pw.print(translateContentDigestAlgorithmIdToString(algorithmId)); |
| pw.print(":"); |
| pw.print(HexEncoding.encodeToString(contentDigest, false)); |
| pw.print("\n"); |
| } |
| } |
| |
| private void printPackageInstallationInfo(PackageInfo packageInfo, |
| boolean useSha256, |
| final PrintWriter pw) { |
| pw.println("--- Package Installation Info ---"); |
| pw.println("Current install location: " |
| + packageInfo.applicationInfo.sourceDir); |
| if (packageInfo.applicationInfo.sourceDir.startsWith("/data/apex/")) { |
| String origPackageFilepath = getOriginalApexPreinstalledLocation( |
| packageInfo.packageName); |
| pw.println("|--> Pre-installed package install location: " |
| + origPackageFilepath); |
| |
| if (!origPackageFilepath.equals(APEX_PRELOAD_LOCATION_ERROR)) { |
| if (useSha256) { |
| String sha256Digest = PackageUtils.computeSha256DigestForLargeFile( |
| origPackageFilepath, PackageUtils.createLargeFileBuffer()); |
| pw.println("|--> Pre-installed package SHA-256 digest: " |
| + sha256Digest); |
| } |
| |
| Map<Integer, byte[]> contentDigests = computeApkContentDigest( |
| origPackageFilepath); |
| if (contentDigests == null) { |
| pw.println("|--> ERROR: Failed to compute package content digest " |
| + "for " + origPackageFilepath); |
| } else { |
| for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { |
| Integer algorithmId = entry.getKey(); |
| byte[] contentDigest = entry.getValue(); |
| pw.println("|--> Pre-installed package content digest: " |
| + HexEncoding.encodeToString(contentDigest, false)); |
| pw.println("|--> Pre-installed package content digest " |
| + "algorithm: " |
| + translateContentDigestAlgorithmIdToString( |
| algorithmId)); |
| } |
| } |
| } |
| } |
| pw.println("First install time (ms): " + packageInfo.firstInstallTime); |
| pw.println("Last update time (ms): " + packageInfo.lastUpdateTime); |
| // TODO(b/261493591): Determination of whether a package is preinstalled can be |
| // made more robust |
| boolean isPreloaded = (packageInfo.firstInstallTime |
| == packageInfo.lastUpdateTime); |
| pw.println("Is preloaded: " + isPreloaded); |
| |
| InstallSourceInfo installSourceInfo = getInstallSourceInfo( |
| packageInfo.packageName); |
| if (installSourceInfo == null) { |
| pw.println("ERROR: Unable to obtain installSourceInfo of " |
| + packageInfo.packageName); |
| } else { |
| pw.println("Installation initiated by: " |
| + installSourceInfo.getInitiatingPackageName()); |
| pw.println("Installation done by: " |
| + installSourceInfo.getInstallingPackageName()); |
| pw.println("Installation originating from: " |
| + installSourceInfo.getOriginatingPackageName()); |
| } |
| |
| if (packageInfo.isApex) { |
| pw.println("Is an active APEX: " + packageInfo.isActiveApex); |
| } |
| } |
| |
| private void printPackageSignerDetails(SigningInfo signerInfo, |
| final PrintWriter pw) { |
| if (signerInfo == null) { |
| pw.println("ERROR: Package's signingInfo is null."); |
| return; |
| } |
| pw.println("--- Package Signer Info ---"); |
| pw.println("Has multiple signers: " + signerInfo.hasMultipleSigners()); |
| pw.println("Signing key has been rotated: " |
| + signerInfo.hasPastSigningCertificates()); |
| Signature[] packageSigners = signerInfo.getApkContentsSigners(); |
| for (Signature packageSigner : packageSigners) { |
| byte[] packageSignerDigestBytes = |
| PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray()); |
| String packageSignerDigestHextring = |
| HexEncoding.encodeToString(packageSignerDigestBytes, false); |
| pw.println("Signer cert's SHA256-digest: " + packageSignerDigestHextring); |
| try { |
| PublicKey publicKey = packageSigner.getPublicKey(); |
| pw.println("Signing key algorithm: " + publicKey.getAlgorithm()); |
| } catch (CertificateException e) { |
| Slog.e(TAG, |
| "Failed to obtain public key of signer for cert with hash: " |
| + packageSignerDigestHextring, e); |
| } |
| } |
| |
| if (!signerInfo.hasMultipleSigners() |
| && signerInfo.hasPastSigningCertificates()) { |
| pw.println("== Signing Cert Lineage (Excluding The Most Recent) =="); |
| pw.println("(Certs are sorted in the order of rotation, beginning with the " |
| + "original signing cert)"); |
| Signature[] signingCertHistory = signerInfo.getSigningCertificateHistory(); |
| for (int i = 0; i < (signingCertHistory.length - 1); i++) { |
| Signature signature = signingCertHistory[i]; |
| byte[] signatureDigestBytes = PackageUtils.computeSha256DigestBytes( |
| signature.toByteArray()); |
| String certHashHexString = HexEncoding.encodeToString( |
| signatureDigestBytes, false); |
| pw.println(" ++ Signer cert #" + (i + 1) + " ++"); |
| pw.println(" Cert SHA256-digest: " + certHashHexString); |
| try { |
| PublicKey publicKey = signature.getPublicKey(); |
| pw.println(" Signing key algorithm: " + publicKey.getAlgorithm()); |
| } catch (CertificateException e) { |
| Slog.e(TAG, "Failed to obtain public key of signer for cert " |
| + "with hash: " + certHashHexString, e); |
| } |
| } |
| } |
| |
| } |
| |
| private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) { |
| pw.println("--- Module Details ---"); |
| pw.println("Module name: " + moduleInfo.getName()); |
| pw.println("Module visibility: " |
| + (moduleInfo.isHidden() ? "hidden" : "visible")); |
| } |
| |
| private void printAppDetails(PackageInfo packageInfo, |
| boolean printLibraries, |
| final PrintWriter pw) { |
| pw.println("--- App Details ---"); |
| pw.println("Name: " + packageInfo.applicationInfo.name); |
| pw.println("Label: " + mContext.getPackageManager().getApplicationLabel( |
| packageInfo.applicationInfo)); |
| pw.println("Description: " + packageInfo.applicationInfo.loadDescription( |
| mContext.getPackageManager())); |
| pw.println("Has code: " + packageInfo.applicationInfo.hasCode()); |
| pw.println("Is enabled: " + packageInfo.applicationInfo.enabled); |
| pw.println("Is suspended: " + ((packageInfo.applicationInfo.flags |
| & ApplicationInfo.FLAG_SUSPENDED) != 0)); |
| |
| pw.println("Compile SDK version: " + packageInfo.compileSdkVersion); |
| pw.println("Target SDK version: " |
| + packageInfo.applicationInfo.targetSdkVersion); |
| |
| pw.println("Is privileged: " |
| + packageInfo.applicationInfo.isPrivilegedApp()); |
| pw.println("Is a stub: " + packageInfo.isStub); |
| pw.println("Is a core app: " + packageInfo.coreApp); |
| pw.println("SEInfo: " + packageInfo.applicationInfo.seInfo); |
| pw.println("Component factory: " |
| + packageInfo.applicationInfo.appComponentFactory); |
| pw.println("Process name: " + packageInfo.applicationInfo.processName); |
| pw.println("Task affinity: " + packageInfo.applicationInfo.taskAffinity); |
| pw.println("UID: " + packageInfo.applicationInfo.uid); |
| pw.println("Shared UID: " + packageInfo.sharedUserId); |
| |
| if (printLibraries) { |
| pw.println("== App's Shared Libraries =="); |
| List<SharedLibraryInfo> sharedLibraryInfos = |
| packageInfo.applicationInfo.getSharedLibraryInfos(); |
| if (sharedLibraryInfos == null || sharedLibraryInfos.isEmpty()) { |
| pw.println("<none>"); |
| } |
| |
| for (int i = 0; i < sharedLibraryInfos.size(); i++) { |
| SharedLibraryInfo sharedLibraryInfo = sharedLibraryInfos.get(i); |
| pw.println(" ++ Library #" + (i + 1) + " ++"); |
| pw.println(" Lib name: " + sharedLibraryInfo.getName()); |
| long libVersion = sharedLibraryInfo.getLongVersion(); |
| pw.print(" Lib version: "); |
| if (libVersion == SharedLibraryInfo.VERSION_UNDEFINED) { |
| pw.print("undefined"); |
| } else { |
| pw.print(libVersion); |
| } |
| pw.print("\n"); |
| |
| pw.println(" Lib package name (if available): " |
| + sharedLibraryInfo.getPackageName()); |
| pw.println(" Lib path: " + sharedLibraryInfo.getPath()); |
| pw.print(" Lib type: "); |
| switch (sharedLibraryInfo.getType()) { |
| case SharedLibraryInfo.TYPE_BUILTIN: |
| pw.print("built-in"); |
| break; |
| case SharedLibraryInfo.TYPE_DYNAMIC: |
| pw.print("dynamic"); |
| break; |
| case SharedLibraryInfo.TYPE_STATIC: |
| pw.print("static"); |
| break; |
| case SharedLibraryInfo.TYPE_SDK_PACKAGE: |
| pw.print("SDK"); |
| break; |
| case SharedLibraryInfo.VERSION_UNDEFINED: |
| default: |
| pw.print("undefined"); |
| break; |
| } |
| pw.print("\n"); |
| pw.println(" Is a native lib: " + sharedLibraryInfo.isNative()); |
| } |
| } |
| |
| } |
| |
| private void printHeadersHelper(@NonNull String packageType, |
| boolean useSha256, |
| @NonNull final PrintWriter pw) { |
| pw.print(packageType + " Info [Format: package_name,package_version,"); |
| if (useSha256) { |
| pw.print("package_sha256_digest,"); |
| } |
| pw.print("content_digest_algorithm:content_digest]:\n"); |
| } |
| |
| private int printAllApexs() { |
| final PrintWriter pw = getOutPrintWriter(); |
| boolean verbose = false; |
| boolean useSha256 = false; |
| boolean printHeaders = true; |
| String opt; |
| while ((opt = getNextOption()) != null) { |
| switch (opt) { |
| case "-v": |
| case "--verbose": |
| verbose = true; |
| break; |
| case "-o": |
| case "--old": |
| useSha256 = true; |
| break; |
| case "--no-headers": |
| printHeaders = false; |
| break; |
| default: |
| pw.println("ERROR: Unknown option: " + opt); |
| return 1; |
| } |
| } |
| |
| PackageManager pm = mContext.getPackageManager(); |
| if (pm == null) { |
| pw.println("ERROR: Failed to obtain an instance of package manager."); |
| return -1; |
| } |
| |
| if (!verbose && printHeaders) { |
| printHeadersHelper("APEX", useSha256, pw); |
| } |
| for (PackageInfo packageInfo : getCurrentInstalledApexs()) { |
| if (verbose && printHeaders) { |
| printHeadersHelper("APEX", useSha256, pw); |
| } |
| String packageName = packageInfo.packageName; |
| pw.print(packageName + "," |
| + packageInfo.getLongVersionCode() + ","); |
| printPackageMeasurements(packageInfo, useSha256, pw); |
| |
| if (verbose) { |
| ModuleInfo moduleInfo; |
| try { |
| moduleInfo = pm.getModuleInfo(packageInfo.packageName, 0); |
| pw.println("Is a module: true"); |
| printModuleDetails(moduleInfo, pw); |
| } catch (PackageManager.NameNotFoundException e) { |
| pw.println("Is a module: false"); |
| } |
| |
| printPackageInstallationInfo(packageInfo, useSha256, pw); |
| printPackageSignerDetails(packageInfo.signingInfo, pw); |
| pw.println(""); |
| } |
| } |
| return 0; |
| } |
| |
| private int printAllModules() { |
| final PrintWriter pw = getOutPrintWriter(); |
| boolean verbose = false; |
| boolean useSha256 = false; |
| boolean printHeaders = true; |
| String opt; |
| while ((opt = getNextOption()) != null) { |
| switch (opt) { |
| case "-v": |
| case "--verbose": |
| verbose = true; |
| break; |
| case "-o": |
| case "--old": |
| useSha256 = true; |
| break; |
| case "--no-headers": |
| printHeaders = false; |
| break; |
| default: |
| pw.println("ERROR: Unknown option: " + opt); |
| return 1; |
| } |
| } |
| |
| PackageManager pm = mContext.getPackageManager(); |
| if (pm == null) { |
| pw.println("ERROR: Failed to obtain an instance of package manager."); |
| return -1; |
| } |
| |
| if (!verbose && printHeaders) { |
| printHeadersHelper("Module", useSha256, pw); |
| } |
| for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) { |
| String packageName = module.getPackageName(); |
| if (verbose && printHeaders) { |
| printHeadersHelper("Module", useSha256, pw); |
| } |
| try { |
| PackageInfo packageInfo = pm.getPackageInfo(packageName, |
| PackageManager.MATCH_APEX |
| | PackageManager.GET_SIGNING_CERTIFICATES); |
| pw.print(packageInfo.packageName + ","); |
| pw.print(packageInfo.getLongVersionCode() + ","); |
| printPackageMeasurements(packageInfo, useSha256, pw); |
| |
| if (verbose) { |
| printModuleDetails(module, pw); |
| printPackageInstallationInfo(packageInfo, useSha256, pw); |
| printPackageSignerDetails(packageInfo.signingInfo, pw); |
| pw.println(""); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| pw.println(packageName |
| + ",ERROR:Unable to find PackageInfo for this module."); |
| if (verbose) { |
| printModuleDetails(module, pw); |
| pw.println(""); |
| } |
| continue; |
| } |
| } |
| return 0; |
| } |
| |
| private int printAllMbas() { |
| final PrintWriter pw = getOutPrintWriter(); |
| boolean verbose = false; |
| boolean printLibraries = false; |
| boolean useSha256 = false; |
| boolean printHeaders = true; |
| boolean preloadsOnly = false; |
| String opt; |
| while ((opt = getNextOption()) != null) { |
| switch (opt) { |
| case "-v": |
| case "--verbose": |
| verbose = true; |
| break; |
| case "-l": |
| printLibraries = true; |
| break; |
| case "-o": |
| case "--old": |
| useSha256 = true; |
| break; |
| case "--no-headers": |
| printHeaders = false; |
| break; |
| case "--preloads-only": |
| preloadsOnly = true; |
| break; |
| default: |
| pw.println("ERROR: Unknown option: " + opt); |
| return 1; |
| } |
| } |
| |
| if (!verbose && printHeaders) { |
| if (preloadsOnly) { |
| printHeadersHelper("Preload", useSha256, pw); |
| } else { |
| printHeadersHelper("MBA", useSha256, pw); |
| } |
| } |
| |
| PackageManager pm = mContext.getPackageManager(); |
| for (PackageInfo packageInfo : pm.getInstalledPackages( |
| PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY |
| | PackageManager.GET_SIGNING_CERTIFICATES))) { |
| if (packageInfo.signingInfo == null) { |
| PackageInfo origPackageInfo = packageInfo; |
| try { |
| pm.getPackageInfo(packageInfo.packageName, |
| PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL |
| | PackageManager.GET_SIGNING_CERTIFICATES)); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, "Failed to obtain an updated PackageInfo of " |
| + origPackageInfo.packageName); |
| packageInfo = origPackageInfo; |
| } |
| } |
| |
| if (verbose && printHeaders) { |
| printHeadersHelper("Preload", useSha256, pw); |
| } |
| pw.print(packageInfo.packageName + ","); |
| pw.print(packageInfo.getLongVersionCode() + ","); |
| printPackageMeasurements(packageInfo, useSha256, pw); |
| |
| if (verbose) { |
| printAppDetails(packageInfo, printLibraries, pw); |
| printPackageInstallationInfo(packageInfo, useSha256, pw); |
| printPackageSignerDetails(packageInfo.signingInfo, pw); |
| pw.println(""); |
| } |
| } |
| |
| if (preloadsOnly) { |
| return 0; |
| } |
| for (PackageInfo packageInfo : getNewlyInstalledMbas()) { |
| if (verbose && printHeaders) { |
| printHeadersHelper("MBA", useSha256, pw); |
| } |
| pw.print(packageInfo.packageName + ","); |
| pw.print(packageInfo.getLongVersionCode() + ","); |
| printPackageMeasurements(packageInfo, useSha256, pw); |
| |
| if (verbose) { |
| printAppDetails(packageInfo, printLibraries, pw); |
| printPackageInstallationInfo(packageInfo, useSha256, pw); |
| printPackageSignerDetails(packageInfo.signingInfo, pw); |
| pw.println(""); |
| } |
| } |
| return 0; |
| } |
| |
| @Override |
| public int onCommand(String cmd) { |
| if (cmd == null) { |
| return handleDefaultCommands(cmd); |
| } |
| |
| final PrintWriter pw = getOutPrintWriter(); |
| switch (cmd) { |
| case "get": { |
| final String infoType = getNextArg(); |
| if (infoType == null) { |
| printHelpMenu(); |
| return -1; |
| } |
| |
| switch (infoType) { |
| case "image_info": |
| return printSignedImageInfo(); |
| case "apex_info": |
| return printAllApexs(); |
| case "module_info": |
| return printAllModules(); |
| case "mba_info": |
| return printAllMbas(); |
| default: |
| pw.println(String.format("ERROR: Unknown info type '%s'", |
| infoType)); |
| return 1; |
| } |
| } |
| default: |
| return handleDefaultCommands(cmd); |
| } |
| } |
| |
| private void printHelpMenu() { |
| final PrintWriter pw = getOutPrintWriter(); |
| pw.println("Transparency manager (transparency) commands:"); |
| pw.println(" help"); |
| pw.println(" Print this help text."); |
| pw.println(""); |
| pw.println(" get image_info [-a]"); |
| pw.println(" Print information about loaded image (firmware). Options:"); |
| pw.println(" -a: lists all other identifiable partitions."); |
| pw.println(""); |
| pw.println(" get apex_info [-o] [-v] [--no-headers]"); |
| pw.println(" Print information about installed APEXs on device."); |
| pw.println(" -o: also uses the old digest scheme (SHA256) to compute " |
| + "APEX hashes. WARNING: This can be a very slow and CPU-intensive " |
| + "computation."); |
| pw.println(" -v: lists more verbose information about each APEX."); |
| pw.println(" --no-headers: does not print the header if specified."); |
| pw.println(""); |
| pw.println(" get module_info [-o] [-v] [--no-headers]"); |
| pw.println(" Print information about installed modules on device."); |
| pw.println(" -o: also uses the old digest scheme (SHA256) to compute " |
| + "module hashes. WARNING: This can be a very slow and " |
| + "CPU-intensive computation."); |
| pw.println(" -v: lists more verbose information about each module."); |
| pw.println(" --no-headers: does not print the header if specified."); |
| pw.println(""); |
| pw.println(" get mba_info [-o] [-v] [-l] [--no-headers] [--preloads-only]"); |
| pw.println(" Print information about installed mobile bundle apps " |
| + "(MBAs on device)."); |
| pw.println(" -o: also uses the old digest scheme (SHA256) to compute " |
| + "MBA hashes. WARNING: This can be a very slow and CPU-intensive " |
| + "computation."); |
| pw.println(" -v: lists more verbose information about each app."); |
| pw.println(" -l: lists shared library info. (This option only works " |
| + "when -v option is also specified)"); |
| pw.println(" --no-headers: does not print the header if specified."); |
| pw.println(" --preloads-only: lists only preloaded apps. This options can " |
| + "also be combined with others."); |
| pw.println(""); |
| } |
| |
| @Override |
| public void onHelp() { |
| printHelpMenu(); |
| } |
| }).exec(this, in, out, err, args, callback, resultReceiver); |
| } |
| } |
| private final BinaryTransparencyServiceImpl mServiceImpl; |
| |
| /** |
| * A wrapper of {@link FrameworkStatsLog} for easier testing |
| */ |
| @VisibleForTesting |
| public static class BiometricLogger { |
| private static final String TAG = "BiometricLogger"; |
| |
| private static final BiometricLogger sInstance = new BiometricLogger(); |
| |
| private BiometricLogger() {} |
| |
| public static BiometricLogger getInstance() { |
| return sInstance; |
| } |
| |
| /** |
| * A wrapper of {@link FrameworkStatsLog} |
| * |
| * @param sensorId The sensorId of the biometric to be logged |
| * @param modality The modality of the biometric |
| * @param sensorType The sensor type of the biometric |
| * @param sensorStrength The sensor strength of the biometric |
| * @param componentId The component Id of a component of the biometric |
| * @param hardwareVersion The hardware version of a component of the biometric |
| * @param firmwareVersion The firmware version of a component of the biometric |
| * @param serialNumber The serial number of a component of the biometric |
| * @param softwareVersion The software version of a component of the biometric |
| */ |
| public void logStats(int sensorId, int modality, int sensorType, int sensorStrength, |
| String componentId, String hardwareVersion, String firmwareVersion, |
| String serialNumber, String softwareVersion) { |
| FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_PROPERTIES_COLLECTED, |
| sensorId, modality, sensorType, sensorStrength, componentId, hardwareVersion, |
| firmwareVersion, serialNumber, softwareVersion); |
| } |
| } |
| |
| public BinaryTransparencyService(Context context) { |
| this(context, BiometricLogger.getInstance()); |
| } |
| |
| @VisibleForTesting |
| BinaryTransparencyService(Context context, BiometricLogger biometricLogger) { |
| super(context); |
| mContext = context; |
| mServiceImpl = new BinaryTransparencyServiceImpl(); |
| mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED; |
| mMeasurementsLastRecordedMs = 0; |
| mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); |
| mBiometricLogger = biometricLogger; |
| } |
| |
| /** |
| * Called when the system service should publish a binder service using |
| * {@link #publishBinderService(String, IBinder).} |
| */ |
| @Override |
| public void onStart() { |
| try { |
| publishBinderService(Context.BINARY_TRANSPARENCY_SERVICE, mServiceImpl); |
| Slog.i(TAG, "Started BinaryTransparencyService"); |
| } catch (Throwable t) { |
| Slog.e(TAG, "Failed to start BinaryTransparencyService.", t); |
| } |
| } |
| |
| /** |
| * Called on each phase of the boot process. Phases before the service's start phase |
| * (as defined in the @Service annotation) are never received. |
| * |
| * @param phase The current boot phase. |
| */ |
| @Override |
| public void onBootPhase(int phase) { |
| |
| // we are only interested in doing things at PHASE_BOOT_COMPLETED |
| if (phase == PHASE_BOOT_COMPLETED) { |
| Slog.i(TAG, "Boot completed. Getting VBMeta Digest."); |
| getVBMetaDigestInformation(); |
| |
| // Log to statsd |
| // TODO(b/264061957): For now, biometric system properties are always collected if users |
| // share usage & diagnostics information. In the future, collect biometric system |
| // properties only when transparency log verification of the target partitions fails |
| // (e.g. when the system/vendor partitions have been changed) once the binary |
| // transparency infrastructure is ready. |
| Slog.i(TAG, "Boot completed. Collecting biometric system properties."); |
| collectBiometricProperties(); |
| |
| // to avoid the risk of holding up boot time, computations to measure APEX, Module, and |
| // MBA digests are scheduled here, but only executed when the device is idle and plugged |
| // in. |
| Slog.i(TAG, "Scheduling measurements to be taken."); |
| UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, |
| BinaryTransparencyService.this); |
| |
| registerAllPackageUpdateObservers(); |
| } |
| } |
| |
| /** |
| * JobService to measure all covered binaries and record results to statsd. |
| */ |
| public static class UpdateMeasurementsJobService extends JobService { |
| private static long sTimeLastRanMs = 0; |
| private static final int DO_BINARY_MEASUREMENTS_JOB_ID = 1740526926; |
| |
| @Override |
| public boolean onStartJob(JobParameters params) { |
| Slog.d(TAG, "Job to update binary measurements started."); |
| if (params.getJobId() != DO_BINARY_MEASUREMENTS_JOB_ID) { |
| return false; |
| } |
| |
| // we'll perform binary measurements via threads to be mindful of low-end devices |
| // where this operation might take longer than expected, and so that we don't block |
| // system_server's main thread. |
| Executors.defaultThreadFactory().newThread(() -> { |
| IBinder b = ServiceManager.getService(Context.BINARY_TRANSPARENCY_SERVICE); |
| IBinaryTransparencyService iBtsService = |
| IBinaryTransparencyService.Stub.asInterface(b); |
| try { |
| iBtsService.recordMeasurementsForAllPackages(); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Taking binary measurements was interrupted.", e); |
| return; |
| } |
| sTimeLastRanMs = System.currentTimeMillis(); |
| jobFinished(params, false); |
| }).start(); |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onStopJob(JobParameters params) { |
| return false; |
| } |
| |
| @SuppressLint("DefaultLocale") |
| static void scheduleBinaryMeasurements(Context context, BinaryTransparencyService service) { |
| Slog.i(TAG, "Scheduling binary content-digest computation job"); |
| final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); |
| if (jobScheduler == null) { |
| Slog.e(TAG, "Failed to obtain an instance of JobScheduler."); |
| return; |
| } |
| |
| if (jobScheduler.getPendingJob(DO_BINARY_MEASUREMENTS_JOB_ID) != null) { |
| Slog.d(TAG, "A measurement job has already been scheduled."); |
| return; |
| } |
| |
| long minWaitingPeriodMs = 0; |
| if (sTimeLastRanMs != 0) { |
| minWaitingPeriodMs = RECORD_MEASUREMENTS_COOLDOWN_MS |
| - (System.currentTimeMillis() - sTimeLastRanMs); |
| // bound the range of minWaitingPeriodMs in the case where > 24h has elapsed |
| minWaitingPeriodMs = Math.max(0, |
| Math.min(minWaitingPeriodMs, RECORD_MEASUREMENTS_COOLDOWN_MS)); |
| Slog.d(TAG, "Scheduling the next measurement to be done at least " |
| + minWaitingPeriodMs + "ms from now."); |
| } |
| |
| final JobInfo jobInfo = new JobInfo.Builder(DO_BINARY_MEASUREMENTS_JOB_ID, |
| new ComponentName(context, UpdateMeasurementsJobService.class)) |
| .setRequiresDeviceIdle(true) |
| .setRequiresCharging(true) |
| .setMinimumLatency(minWaitingPeriodMs) |
| .build(); |
| if (jobScheduler.schedule(jobInfo) != JobScheduler.RESULT_SUCCESS) { |
| Slog.e(TAG, "Failed to schedule job to measure binaries."); |
| return; |
| } |
| Slog.d(TAG, TextUtils.formatSimple( |
| "Job %d to measure binaries was scheduled successfully.", |
| DO_BINARY_MEASUREMENTS_JOB_ID)); |
| } |
| } |
| |
| /** |
| * Convert a {@link FingerprintSensorProperties} sensor type to the corresponding enum to be |
| * logged. |
| * |
| * @param sensorType See {@link FingerprintSensorProperties} |
| * @return The enum to be logged |
| */ |
| private int toFingerprintSensorType(@FingerprintSensorProperties.SensorType int sensorType) { |
| switch (sensorType) { |
| case FingerprintSensorProperties.TYPE_REAR: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_REAR; |
| case FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_UDFPS_ULTRASONIC; |
| case FingerprintSensorProperties.TYPE_UDFPS_OPTICAL: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_UDFPS_OPTICAL; |
| case FingerprintSensorProperties.TYPE_POWER_BUTTON: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_POWER_BUTTON; |
| case FingerprintSensorProperties.TYPE_HOME_BUTTON: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_HOME_BUTTON; |
| default: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_UNKNOWN; |
| } |
| } |
| |
| /** |
| * Convert a {@link FaceSensorProperties} sensor type to the corresponding enum to be logged. |
| * |
| * @param sensorType See {@link FaceSensorProperties} |
| * @return The enum to be logged |
| */ |
| private int toFaceSensorType(@FaceSensorProperties.SensorType int sensorType) { |
| switch (sensorType) { |
| case FaceSensorProperties.TYPE_RGB: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FACE_RGB; |
| case FaceSensorProperties.TYPE_IR: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FACE_IR; |
| default: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_UNKNOWN; |
| } |
| } |
| |
| /** |
| * Convert a {@link SensorProperties} sensor strength to the corresponding enum to be logged. |
| * |
| * @param sensorStrength See {@link SensorProperties} |
| * @return The enum to be logged |
| */ |
| private int toSensorStrength(@SensorProperties.Strength int sensorStrength) { |
| switch (sensorStrength) { |
| case SensorProperties.STRENGTH_CONVENIENCE: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_CONVENIENCE; |
| case SensorProperties.STRENGTH_WEAK: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_WEAK; |
| case SensorProperties.STRENGTH_STRONG: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_STRONG; |
| default: |
| return FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_UNKNOWN; |
| } |
| } |
| |
| /** |
| * A helper function to log detailed biometric sensor properties to statsd. |
| * |
| * @param prop The biometric sensor properties to be logged |
| * @param modality The modality of the biometric (e.g. fingerprint, face) to be logged |
| * @param sensorType The specific type of the biometric to be logged |
| */ |
| private void logBiometricProperties(SensorProperties prop, int modality, int sensorType) { |
| final int sensorId = prop.getSensorId(); |
| final int sensorStrength = toSensorStrength(prop.getSensorStrength()); |
| |
| // Log data for each component |
| // Note: none of the component info is a device identifier since every device of a given |
| // model and build share the same biometric system info (see b/216195167) |
| for (ComponentInfo componentInfo : prop.getComponentInfo()) { |
| mBiometricLogger.logStats( |
| sensorId, |
| modality, |
| sensorType, |
| sensorStrength, |
| componentInfo.getComponentId().trim(), |
| componentInfo.getHardwareVersion().trim(), |
| componentInfo.getFirmwareVersion().trim(), |
| componentInfo.getSerialNumber().trim(), |
| componentInfo.getSoftwareVersion().trim()); |
| } |
| } |
| |
| @VisibleForTesting |
| void collectBiometricProperties() { |
| // Check the flag to determine whether biometric property verification is enabled. It's |
| // disabled by default. |
| if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BIOMETRICS, |
| KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION, false)) { |
| if (DEBUG) { |
| Slog.d(TAG, "Do not collect/verify biometric properties. Feature disabled by " |
| + "DeviceConfig"); |
| } |
| return; |
| } |
| |
| PackageManager pm = mContext.getPackageManager(); |
| FingerprintManager fpManager = null; |
| FaceManager faceManager = null; |
| if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { |
| fpManager = mContext.getSystemService(FingerprintManager.class); |
| } |
| if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_FACE)) { |
| faceManager = mContext.getSystemService(FaceManager.class); |
| } |
| |
| if (fpManager != null) { |
| final int fpModality = FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__MODALITY__MODALITY_FINGERPRINT; |
| fpManager.addAuthenticatorsRegisteredCallback( |
| new IFingerprintAuthenticatorsRegisteredCallback.Stub() { |
| @Override |
| public void onAllAuthenticatorsRegistered( |
| List<FingerprintSensorPropertiesInternal> sensors) { |
| if (DEBUG) { |
| Slog.d(TAG, "Retrieve fingerprint sensor properties. " |
| + "sensors.size()=" + sensors.size()); |
| } |
| // Log data for each fingerprint sensor |
| for (FingerprintSensorPropertiesInternal propInternal : sensors) { |
| final FingerprintSensorProperties prop = |
| FingerprintSensorProperties.from(propInternal); |
| logBiometricProperties(prop, |
| fpModality, |
| toFingerprintSensorType(prop.getSensorType())); |
| } |
| } |
| }); |
| } |
| |
| if (faceManager != null) { |
| final int faceModality = FrameworkStatsLog |
| .BIOMETRIC_PROPERTIES_COLLECTED__MODALITY__MODALITY_FACE; |
| faceManager.addAuthenticatorsRegisteredCallback( |
| new IFaceAuthenticatorsRegisteredCallback.Stub() { |
| @Override |
| public void onAllAuthenticatorsRegistered( |
| List<FaceSensorPropertiesInternal> sensors) { |
| if (DEBUG) { |
| Slog.d(TAG, "Retrieve face sensor properties. sensors.size()=" |
| + sensors.size()); |
| } |
| // Log data for each face sensor |
| for (FaceSensorPropertiesInternal propInternal : sensors) { |
| final FaceSensorProperties prop = |
| FaceSensorProperties.from(propInternal); |
| logBiometricProperties(prop, |
| faceModality, |
| toFaceSensorType(prop.getSensorType())); |
| } |
| } |
| }); |
| } |
| } |
| |
| private void getVBMetaDigestInformation() { |
| mVbmetaDigest = SystemProperties.get(SYSPROP_NAME_VBETA_DIGEST, VBMETA_DIGEST_UNAVAILABLE); |
| Slog.d(TAG, String.format("VBMeta Digest: %s", mVbmetaDigest)); |
| FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest); |
| } |
| |
| /** |
| * Listen for APK updates. |
| * |
| * There are two ways available to us to do this: |
| * 1. Register an observer using |
| * {@link PackageManagerInternal#getPackageList(PackageManagerInternal.PackageListObserver)}. |
| * 2. Register broadcast receivers, listening to either {@code ACTION_PACKAGE_ADDED} or |
| * {@code ACTION_PACKAGE_REPLACED}. |
| * |
| * After experimentation, we found that Option #1 does not catch updates to non-staged APEXs. |
| * Thus, we are implementing Option #2 here. More specifically, listening to |
| * {@link Intent#ACTION_PACKAGE_ADDED} allows us to capture all events we care about. |
| * |
| * We did not use {@link Intent#ACTION_PACKAGE_REPLACED} because it unfortunately does not |
| * detect updates to non-staged APEXs. Thus, we rely on {@link Intent#EXTRA_REPLACING} to |
| * filter out new installation from updates instead. |
| */ |
| private void registerApkAndNonStagedApexUpdateListener() { |
| Slog.d(TAG, "Registering APK & Non-Staged APEX updates..."); |
| IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); |
| filter.addDataScheme("package"); // this is somehow necessary |
| mContext.registerReceiver(new PackageUpdatedReceiver(), filter); |
| } |
| |
| /** |
| * Listen for staged-APEX updates. |
| * |
| * This method basically covers cases that are not caught by |
| * {@link #registerApkAndNonStagedApexUpdateListener()}, namely updates to APEXs that are staged |
| * for the subsequent reboot. |
| */ |
| private void registerStagedApexUpdateObserver() { |
| Slog.d(TAG, "Registering APEX updates..."); |
| IPackageManagerNative iPackageManagerNative = IPackageManagerNative.Stub.asInterface( |
| ServiceManager.getService("package_native")); |
| if (iPackageManagerNative == null) { |
| Slog.e(TAG, "IPackageManagerNative is null"); |
| return; |
| } |
| |
| try { |
| iPackageManagerNative.registerStagedApexObserver(new IStagedApexObserver.Stub() { |
| @Override |
| public void onApexStaged(ApexStagedEvent event) throws RemoteException { |
| Slog.d(TAG, "A new APEX has been staged for update. There are currently " |
| + event.stagedApexModuleNames.length + " APEX(s) staged for update. " |
| + "Scheduling measurement..."); |
| UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, |
| BinaryTransparencyService.this); |
| } |
| }); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to register a StagedApexObserver."); |
| } |
| } |
| |
| private boolean isPackagePreloaded(String packageName) { |
| PackageManager pm = mContext.getPackageManager(); |
| try { |
| pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of( |
| PackageManager.MATCH_FACTORY_ONLY)); |
| } catch (PackageManager.NameNotFoundException e) { |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean isPackageAnApex(String packageName) { |
| PackageManager pm = mContext.getPackageManager(); |
| try { |
| PackageInfo packageInfo = pm.getPackageInfo(packageName, |
| PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX)); |
| return packageInfo.isApex; |
| } catch (PackageManager.NameNotFoundException e) { |
| return false; |
| } |
| } |
| |
| private class PackageUpdatedReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)) { |
| return; |
| } |
| |
| Uri data = intent.getData(); |
| if (data == null) { |
| Slog.e(TAG, "Shouldn't happen: intent data is null!"); |
| return; |
| } |
| |
| if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { |
| Slog.d(TAG, "Not an update. Skipping..."); |
| return; |
| } |
| |
| String packageName = data.getSchemeSpecificPart(); |
| // now we've got to check what package is this |
| if (isPackagePreloaded(packageName) || isPackageAnApex(packageName)) { |
| Slog.d(TAG, packageName + " was updated. Scheduling measurement..."); |
| UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, |
| BinaryTransparencyService.this); |
| } |
| } |
| } |
| |
| /** |
| * Register observers for APK and APEX updates. The current implementation breaks this process |
| * into 2 cases/methods because PackageManager does not offer a unified interface to register |
| * for all package updates in a universal and comprehensive manner. |
| * Thus, the observers will be invoked when either |
| * i) APK or non-staged APEX update; or |
| * ii) APEX staging happens. |
| * This will then be used as signals to schedule measurement for the relevant binaries. |
| */ |
| private void registerAllPackageUpdateObservers() { |
| registerApkAndNonStagedApexUpdateListener(); |
| registerStagedApexUpdateObserver(); |
| } |
| |
| private String translateContentDigestAlgorithmIdToString(int algorithmId) { |
| switch (algorithmId) { |
| case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256: |
| return "CHUNKED_SHA256"; |
| case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512: |
| return "CHUNKED_SHA512"; |
| case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256: |
| return "VERITY_CHUNKED_SHA256"; |
| case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256: |
| return "SHA256"; |
| default: |
| return "UNKNOWN_ALGO_ID(" + algorithmId + ")"; |
| } |
| } |
| |
| @NonNull |
| private List<PackageInfo> getCurrentInstalledApexs() { |
| List<PackageInfo> results = new ArrayList<>(); |
| PackageManager pm = mContext.getPackageManager(); |
| if (pm == null) { |
| Slog.e(TAG, "Error obtaining an instance of PackageManager."); |
| return results; |
| } |
| List<PackageInfo> allPackages = pm.getInstalledPackages( |
| PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX |
| | PackageManager.GET_SIGNING_CERTIFICATES)); |
| if (allPackages == null) { |
| Slog.e(TAG, "Error obtaining installed packages (including APEX)"); |
| return results; |
| } |
| |
| results = allPackages.stream().filter(p -> p.isApex).collect(Collectors.toList()); |
| return results; |
| } |
| |
| @Nullable |
| private InstallSourceInfo getInstallSourceInfo(String packageName) { |
| PackageManager pm = mContext.getPackageManager(); |
| if (pm == null) { |
| Slog.e(TAG, "Error obtaining an instance of PackageManager."); |
| return null; |
| } |
| try { |
| return pm.getInstallSourceInfo(packageName); |
| } catch (PackageManager.NameNotFoundException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| } |
| |
| @NonNull |
| private String getOriginalApexPreinstalledLocation(String packageName) { |
| try { |
| final String moduleName = apexPackageNameToModuleName(packageName); |
| IApexService apexService = IApexService.Stub.asInterface( |
| Binder.allowBlocking(ServiceManager.waitForService("apexservice"))); |
| for (ApexInfo info : apexService.getAllPackages()) { |
| if (moduleName.equals(info.moduleName)) { |
| return info.preinstalledModulePath; |
| } |
| } |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Unable to get package list from apexservice", e); |
| } |
| return APEX_PRELOAD_LOCATION_ERROR; |
| } |
| |
| private String apexPackageNameToModuleName(String packageName) { |
| // It appears that only apexd knows the preinstalled location, and it uses module name as |
| // the identifier instead of package name. Given the input is a package name, we need to |
| // covert to module name. |
| return ApexManager.getInstance().getApexModuleNameForPackageName(packageName); |
| } |
| |
| /** |
| * Wrapper method to call into IBICS to get a list of all newly installed MBAs. |
| * |
| * We expect IBICS to maintain an accurate list of installed MBAs, and we merely make use of |
| * the results within this service. This means we do not further check whether the |
| * apps in the returned slice is still installed or not, esp. considering that preloaded apps |
| * could be updated, or post-setup installed apps *might* be deleted in real time. |
| * |
| * Note that we do *not* cache the results from IBICS because of the more dynamic nature of |
| * MBAs v.s. other binaries that we measure. |
| * |
| * @return a list of preloaded apps + dynamically installed apps that fit the definition of MBA. |
| */ |
| @NonNull |
| private List<PackageInfo> getNewlyInstalledMbas() { |
| List<PackageInfo> result = new ArrayList<>(); |
| IBackgroundInstallControlService iBics = IBackgroundInstallControlService.Stub.asInterface( |
| ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); |
| if (iBics == null) { |
| Slog.e(TAG, |
| "Failed to obtain an IBinder instance of IBackgroundInstallControlService"); |
| return result; |
| } |
| ParceledListSlice<PackageInfo> slice; |
| try { |
| slice = iBics.getBackgroundInstalledPackages( |
| PackageManager.MATCH_ALL | PackageManager.GET_SIGNING_CERTIFICATES, |
| UserHandle.USER_SYSTEM); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to get a list of MBAs.", e); |
| return result; |
| } |
| return slice.getList(); |
| } |
| |
| private static class Digest { |
| public int algorithm; |
| public byte[] value; |
| |
| Digest(int algorithm, byte[] value) { |
| this.algorithm = algorithm; |
| this.value = value; |
| } |
| } |
| } |