From d891f01d96cbaa3b1329c3d476084f3fedb30a89 Mon Sep 17 00:00:00 2001 From: Mark Rathjen Date: Thu, 19 Jan 2017 04:10:37 +0000 Subject: [PATCH] Roll forward SSAID Migration to be Per App/User Unique Values. SSAID is currently shared across all applications for each user on the device, giving developers the ability to track users across multiple applications. Using SSAID for tracking is an abuse of the original intention of the SSAID and has inherent privacy concerns. This change will make the SSAID unique per application, per user on a device. To not affect applications installed prior to this change they will retain the legacy SSAID value until uninstalled and reinstalled again. Across subsequent installations the application will receive the same SSAID as long as the package name and signature remain consistent. Tested manually the following cases: - App retains the legacy sssaid after OTA. - App gets a new ssaid upon post-OTA installation. - App retrieves same ssaid across post-OTA unistall/reinstalls. - Different Apps receive different ssaids. - Factory reset removes ssaid data and generates a different ssaid after App install. - System retains legacy ssaid. Bug: 34395671 Test: CTS tests passed, Manual testing passed This reverts commit be43257005d086ff7d93c15dae22ac40bc0d545e. Change-Id: Ibf20e7949304c30d65bb8aa24cdbbe6e104b1002 --- core/java/android/util/ByteStringUtils.java | 82 ++++++ core/java/android/util/PackageUtils.java | 13 +- .../provider/SettingsProviderTest.java | 16 ++ .../providers/settings/SettingsProvider.java | 261 +++++++++++++++++- 4 files changed, 355 insertions(+), 17 deletions(-) create mode 100644 core/java/android/util/ByteStringUtils.java diff --git a/core/java/android/util/ByteStringUtils.java b/core/java/android/util/ByteStringUtils.java new file mode 100644 index 0000000000000..7103e6da06250 --- /dev/null +++ b/core/java/android/util/ByteStringUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 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 android.util; + +/** + * A utility class for common byte array to hex string operations and vise versa. + * + * @hide + */ +public final class ByteStringUtils { + private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + private ByteStringUtils() { + /* hide constructor */ + } + + /** + * Returns the hex encoded string representation of bytes. + * @param bytes Byte array to encode. + * @return Hex encoded string representation of bytes. + */ + public static String toString(byte[] bytes) { + if (bytes == null || bytes.length == 0 || bytes.length % 2 != 0) { + return null; + } + + final int byteLength = bytes.length; + final int charCount = 2 * byteLength; + final char[] chars = new char[charCount]; + + for (int i = 0; i < byteLength; i++) { + final int byteHex = bytes[i] & 0xFF; + chars[i * 2] = HEX_ARRAY[byteHex >>> 4]; + chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F]; + } + return new String(chars); + } + + /** + * Returns the decoded byte array representation of str. + * @param str Hex encoded string to decode. + * @return Decoded byte array representation of str. + */ + public static byte[] toByteArray(String str) { + if (str == null || str.length() == 0 || str.length() % 2 != 0) { + return null; + } + + final char[] chars = str.toCharArray(); + final int charLength = chars.length; + final byte[] bytes = new byte[charLength / 2]; + + for (int i = 0; i < bytes.length; i++) { + bytes[i] = + (byte)(((getIndex(chars[i * 2]) << 4) & 0xF0) | (getIndex(chars[i * 2 + 1]) & 0x0F)); + } + return bytes; + } + + private static int getIndex(char c) { + for (int i = 0; i < HEX_ARRAY.length; i++) { + if (HEX_ARRAY[i] == c) { + return i; + } + } + return -1; + } +} diff --git a/core/java/android/util/PackageUtils.java b/core/java/android/util/PackageUtils.java index 6531aef9a50ca..31819797ea17d 100644 --- a/core/java/android/util/PackageUtils.java +++ b/core/java/android/util/PackageUtils.java @@ -30,7 +30,6 @@ import java.security.NoSuchAlgorithmException; * @hide */ public final class PackageUtils { - private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private PackageUtils() { /* hide constructor */ @@ -81,16 +80,6 @@ public final class PackageUtils { messageDigest.update(data); - final byte[] digest = messageDigest.digest(); - final int digestLength = digest.length; - final int charCount = 2 * digestLength; - - final char[] chars = new char[charCount]; - for (int i = 0; i < digestLength; i++) { - final int byteHex = digest[i] & 0xFF; - chars[i * 2] = HEX_ARRAY[byteHex >>> 4]; - chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F]; - } - return new String(chars); + return ByteStringUtils.toString(messageDigest.digest()); } } diff --git a/core/tests/coretests/src/android/provider/SettingsProviderTest.java b/core/tests/coretests/src/android/provider/SettingsProviderTest.java index b0ce2c85d04e1..3fbf169460b2e 100644 --- a/core/tests/coretests/src/android/provider/SettingsProviderTest.java +++ b/core/tests/coretests/src/android/provider/SettingsProviderTest.java @@ -364,4 +364,20 @@ public class SettingsProviderTest extends AndroidTestCase { // one or more activity can handle this intent. assertTrue(resolveInfoList.size() > 0); } + + @SmallTest + public void testValidSsaid() { + ContentResolver r = getContext().getContentResolver(); + + // Verify ssaid + String ssaid = Settings.Secure.getString(r, Settings.Secure.ANDROID_ID); + assertTrue(ssaid != null); + assertTrue(ssaid.length() == 16); + + String ssaid2 = Settings.Secure.getString(r, Settings.Secure.ANDROID_ID); + assertTrue(ssaid2 != null); + assertTrue(ssaid2.length() == 16); + + assertTrue(ssaid.equals(ssaid2)); + } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 3e62158d591cd..058e38a5ede10 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -61,6 +61,7 @@ import android.provider.Settings.Global; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.ByteStringUtils; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -76,9 +77,13 @@ import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -162,6 +167,7 @@ public class SettingsProvider extends ContentProvider { public static final int SETTINGS_TYPE_GLOBAL = 0; public static final int SETTINGS_TYPE_SYSTEM = 1; public static final int SETTINGS_TYPE_SECURE = 2; + public static final int SETTINGS_TYPE_SSAID = 3; public static final int SETTINGS_TYPE_MASK = 0xF0000000; public static final int SETTINGS_TYPE_SHIFT = 28; @@ -249,6 +255,9 @@ public class SettingsProvider extends ContentProvider { case SETTINGS_TYPE_SYSTEM: { return "SETTINGS_SYSTEM"; } + case SETTINGS_TYPE_SSAID: { + return "SETTINGS_SSAID"; + } default: { return "UNKNOWN"; } @@ -704,6 +713,13 @@ public class SettingsProvider extends ContentProvider { UserHandle.getUserId(uid)); } } + + @Override + public void onUidRemoved(int uid) { + synchronized (mLock) { + mSettingsRegistry.onUidRemovedLocked(uid); + } + } }; // package changes @@ -957,8 +973,15 @@ public class SettingsProvider extends ContentProvider { continue; } - Setting setting = mSettingsRegistry.getSettingLocked( - SETTINGS_TYPE_SECURE, owningUserId, name); + // As of Android O (API 24), the SSAID is read from an app-specific entry in table + // SETTINGS_FILE_SSAID, unless accessed by a system process. + final Setting setting; + if (isNewSsaidSetting(name)) { + setting = getSsaidSettingLocked(owningUserId); + } else { + setting = mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SECURE, owningUserId, + name); + } appendSettingToCursor(result, setting); } @@ -986,11 +1009,43 @@ public class SettingsProvider extends ContentProvider { // Get the value. synchronized (mLock) { + // As of Android O (API 24), the SSAID is read from an app-specific entry in table + // SETTINGS_FILE_SSAID, unless accessed by a system process. + if (isNewSsaidSetting(name)) { + return getSsaidSettingLocked(owningUserId); + } + return mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SECURE, owningUserId, name); } } + private boolean isNewSsaidSetting(String name) { + return Settings.Secure.ANDROID_ID.equals(name) + && UserHandle.getAppId(Binder.getCallingUid()) >= Process.FIRST_APPLICATION_UID; + } + + private Setting getSsaidSettingLocked(int owningUserId) { + // Get uid of caller (key) used to store ssaid value + String name = Integer.toString( + UserHandle.getUid(owningUserId, UserHandle.getAppId(Binder.getCallingUid()))); + + if (DEBUG) { + Slog.v(LOG_TAG, "getSsaidSettingLocked(" + name + "," + owningUserId + ")"); + } + + // Retrieve the ssaid from the table if present. + final Setting ssaid = mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_SSAID, owningUserId, + name); + + // Lazy initialize ssaid if not yet present in ssaid table. + if (ssaid.isNull() || ssaid.getValue() == null) { + return mSettingsRegistry.generateSsaidLocked(getCallingPackage(), owningUserId); + } + + return ssaid; + } + private boolean insertSecureSetting(String name, String value, String tag, boolean makeDefault, int requestingUserId, boolean forceNotify) { if (DEBUG) { @@ -1818,6 +1873,9 @@ public class SettingsProvider extends ContentProvider { private static final String SETTINGS_FILE_GLOBAL = "settings_global.xml"; private static final String SETTINGS_FILE_SYSTEM = "settings_system.xml"; private static final String SETTINGS_FILE_SECURE = "settings_secure.xml"; + private static final String SETTINGS_FILE_SSAID = "settings_ssaid.xml"; + + private static final String SSAID_USER_KEY = "userkey"; private final SparseArray mSettingsStates = new SparseArray<>(); @@ -1832,6 +1890,115 @@ public class SettingsProvider extends ContentProvider { mGenerationRegistry = new GenerationRegistry(mLock); mBackupManager = new BackupManager(getContext()); migrateAllLegacySettingsIfNeeded(); + syncSsaidTableOnStart(); + } + + private void generateUserKeyLocked(int userId) { + // Generate a random key for each user used for creating a new ssaid. + final byte[] keyBytes = new byte[16]; + final SecureRandom rand = new SecureRandom(); + rand.nextBytes(keyBytes); + + // Convert to string for storage in settings table. + final String userKey = ByteStringUtils.toString(keyBytes); + + // Store the key in the ssaid table. + final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID, userId); + final boolean success = ssaidSettings.insertSettingLocked(SSAID_USER_KEY, userKey, null, + true, SettingsState.SYSTEM_PACKAGE_NAME); + + if (!success) { + throw new IllegalStateException("Ssaid settings not accessible"); + } + } + + public Setting generateSsaidLocked(String packageName, int userId) { + final PackageInfo packageInfo; + try { + packageInfo = mPackageManager.getPackageInfo(packageName, + PackageManager.GET_SIGNATURES, userId); + } catch (RemoteException e) { + throw new IllegalStateException("Package info doesn't exist"); + } + + // Read the user's key from the ssaid table. + Setting userKeySetting = getSettingLocked(SETTINGS_TYPE_SSAID, userId, SSAID_USER_KEY); + if (userKeySetting.isNull() || userKeySetting.getValue() == null) { + // Lazy initialize and store the user key. + generateUserKeyLocked(userId); + userKeySetting = getSettingLocked(SETTINGS_TYPE_SSAID, userId, SSAID_USER_KEY); + if (userKeySetting.isNull() || userKeySetting.getValue() == null) { + throw new IllegalStateException("User key not accessible"); + } + } + final String userKey = userKeySetting.getValue(); + + // Convert the user's key back to a byte array. + final byte[] keyBytes = ByteStringUtils.toByteArray(userKey); + if (keyBytes == null || keyBytes.length != 16) { + throw new IllegalStateException("User key invalid"); + } + + final MessageDigest md; + try { + // Hash package name and signature. + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("HmacSHA256 is not available"); + } + md.update(keyBytes); + md.update(packageInfo.packageName.getBytes(StandardCharsets.UTF_8)); + md.update(packageInfo.signatures[0].toByteArray()); + + // Convert result to a string for storage in settings table. Only want first 64 bits. + final String ssaid = ByteStringUtils.toString(md.digest()).substring(0, 16) + .toLowerCase(); + + // Save the ssaid in the ssaid table. + final String uid = Integer.toString(packageInfo.applicationInfo.uid); + final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID, userId); + final boolean success = ssaidSettings.insertSettingLocked(uid, ssaid, null, true, + packageName); + + if (!success) { + throw new IllegalStateException("Ssaid settings not accessible"); + } + + return getSettingLocked(SETTINGS_TYPE_SSAID, userId, uid); + } + + public void syncSsaidTableOnStart() { + synchronized (mLock) { + // Verify that each user's packages and ssaid's are in sync. + for (UserInfo user : mUserManager.getUsers(true)) { + // Get all uids for the user's packages. + final List packages; + try { + packages = mPackageManager.getInstalledPackages(0, user.id).getList(); + } catch (RemoteException e) { + throw new IllegalStateException("Package manager not available"); + } + final Set appUids = new HashSet<>(); + for (PackageInfo info : packages) { + appUids.add(Integer.toString(info.applicationInfo.uid)); + } + + // Get all uids currently stored in the user's ssaid table. + final Set ssaidUids = new HashSet<>( + getSettingsNamesLocked(SETTINGS_TYPE_SSAID, user.id)); + ssaidUids.remove(SSAID_USER_KEY); + + // Perform a set difference for the appUids and ssaidUids. + ssaidUids.removeAll(appUids); + + // If there are ssaidUids left over they need to be removed from the table. + final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID, + user.id); + for (String uid : ssaidUids) { + ssaidSettings.deleteSettingLocked(uid); + } + } + } } public List getSettingsNamesLocked(int type, int userId) { @@ -1884,6 +2051,10 @@ public class SettingsProvider extends ContentProvider { final int systemKey = makeKey(SETTINGS_TYPE_SYSTEM, userId); ensureSettingsStateLocked(systemKey); + // Ensure secure settings loaded. + final int ssaidKey = makeKey(SETTINGS_TYPE_SSAID, userId); + ensureSettingsStateLocked(ssaidKey); + // Upgrade the settings to the latest version. UpgradeController upgrader = new UpgradeController(userId); upgrader.upgradeIfNeededLocked(); @@ -1936,6 +2107,23 @@ public class SettingsProvider extends ContentProvider { } } + // Nuke ssaid settings. + final int ssaidKey = makeKey(SETTINGS_TYPE_SSAID, userId); + final SettingsState ssaidSettingsState = mSettingsStates.get(ssaidKey); + if (ssaidSettingsState != null) { + if (permanently) { + mSettingsStates.remove(ssaidKey); + ssaidSettingsState.destroyLocked(null); + } else { + ssaidSettingsState.destroyLocked(new Runnable() { + @Override + public void run() { + mSettingsStates.remove(ssaidKey); + } + }); + } + } + // Nuke generation tracking data mGenerationRegistry.onUserRemoved(userId); } @@ -1977,8 +2165,10 @@ public class SettingsProvider extends ContentProvider { SettingsState settingsState = peekSettingsStateLocked(key); if (settingsState == null) { - return null; + return settingsState.getNullSetting(); } + + // getSettingLocked will return non-null result return settingsState.getSettingLocked(name); } @@ -2079,6 +2269,12 @@ public class SettingsProvider extends ContentProvider { } } + public void onUidRemovedLocked(int uid) { + final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID, + UserHandle.getUserId(uid)); + ssaidSettings.deleteSettingLocked(Integer.toString(uid)); + } + private SettingsState peekSettingsStateLocked(int key) { SettingsState settingsState = mSettingsStates.get(key); if (settingsState != null) { @@ -2300,6 +2496,10 @@ public class SettingsProvider extends ContentProvider { return getTypeFromKey(key) == SETTINGS_TYPE_SECURE; } + private boolean isSsaidSettingsKey(int key) { + return getTypeFromKey(key) == SETTINGS_TYPE_SSAID; + } + private File getSettingsFile(int key) { if (isGlobalSettingsKey(key)) { final int userId = getUserIdFromKey(key); @@ -2313,6 +2513,10 @@ public class SettingsProvider extends ContentProvider { final int userId = getUserIdFromKey(key); return new File(Environment.getUserSystemDirectory(userId), SETTINGS_FILE_SECURE); + } else if (isSsaidSettingsKey(key)) { + final int userId = getUserIdFromKey(key); + return new File(Environment.getUserSystemDirectory(userId), + SETTINGS_FILE_SSAID); } else { throw new IllegalArgumentException("Invalid settings key:" + key); } @@ -2336,7 +2540,8 @@ public class SettingsProvider extends ContentProvider { private int getMaxBytesPerPackageForType(int type) { switch (type) { case SETTINGS_TYPE_GLOBAL: - case SETTINGS_TYPE_SECURE: { + case SETTINGS_TYPE_SECURE: + case SETTINGS_TYPE_SSAID: { return SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED; } @@ -2374,7 +2579,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 136; + private static final int SETTINGS_VERSION = 137; private final int mUserId; @@ -2447,6 +2652,10 @@ public class SettingsProvider extends ContentProvider { return getSettingsLocked(SETTINGS_TYPE_SECURE, userId); } + private SettingsState getSsaidSettingsLocked(int userId) { + return getSettingsLocked(SETTINGS_TYPE_SSAID, userId); + } + private SettingsState getSystemSettingsLocked(int userId) { return getSettingsLocked(SETTINGS_TYPE_SYSTEM, userId); } @@ -2776,6 +2985,48 @@ public class SettingsProvider extends ContentProvider { currentVersion = 136; } + if (currentVersion == 136) { + // Version 136: Store legacy SSAID for all apps currently installed on the + // device as first step in migrating SSAID to be unique per application. + + final boolean isUpgrade; + try { + isUpgrade = mPackageManager.isUpgrade(); + } catch (RemoteException e) { + throw new IllegalStateException("Package manager not available"); + } + // Only retain legacy ssaid if the device is performing an OTA. After wiping + // user data or first boot on a new device should use new ssaid generation. + if (isUpgrade) { + // Retrieve the legacy ssaid from the secure settings table. + final String legacySsaid = getSettingLocked(SETTINGS_TYPE_SECURE, userId, + Settings.Secure.ANDROID_ID).getValue(); + + // Fill each uid with the legacy ssaid to be backwards compatible. + final List packages; + try { + packages = mPackageManager.getInstalledPackages(0, userId).getList(); + } catch (RemoteException e) { + throw new IllegalStateException("Package manager not available"); + } + + final SettingsState ssaidSettings = getSsaidSettingsLocked(userId); + for (PackageInfo info : packages) { + // Check if the UID already has an entry in the table. + final String uid = Integer.toString(info.applicationInfo.uid); + final Setting ssaid = ssaidSettings.getSettingLocked(uid); + + if (ssaid.isNull() || ssaid.getValue() == null) { + // Android Id doesn't exist for this package so create it. + ssaidSettings.insertSettingLocked(uid, legacySsaid, null, true, + info.packageName); + } + } + } + + currentVersion = 137; + } + if (currentVersion != newVersion) { Slog.wtf("SettingsProvider", "warning: upgrading settings database to version " + newVersion + " left it at "