From 011f5c6ea5af27867a2ca339e2dfbcb6a7fcb65e Mon Sep 17 00:00:00 2001 From: Alex Johnston Date: Wed, 18 Dec 2019 17:05:57 +0000 Subject: Add new Factory reset protection policy APIs Historically, FRP behaviour was built on top of the application restrictions infrastructure. This CL introduces new behaviour, as the profile owner of an organization-owned device needs to be able to control FRP behaviour and cannot set application restrictions on user 0. - Introduced a new FactoryResetProtectionPolicy object - Added it as a field to ActiveAdmin - Created a default value - Added setFactoryResetProtectionPolicy to DPM which is used to set a factory reset protection policy. If the policy is null, the current policy is cleared and set back to the default value. - Added getFactoryResetProtectionPolicy to DPM to retrieve the current factory reset protection policy. If this is the default value, then no policy is set. - Both these APIs are callable by the device owner or profile owner of an organization-owned device. Bug: 143517230 Test: atest com.android.server.devicepolicy.DevicePolicyManagerTest atest com.android.server.devicepolicy.FactoryResetProtectionPolicyTest atest com.android.cts.devicepolicy.MixedDeviceOwnerTest#testFactoryResetProtectionPolicy atest com.android.cts.devicepolicy.OrgOwnedProfileOwnerTest#testFactoryResetProtectionPolicy Change-Id: I72c4d06ec8a27741a1956d082969573077937726 --- api/current.txt | 17 ++ api/system-current.txt | 1 + .../android/app/admin/DevicePolicyManager.java | 64 ++++++ .../app/admin/FactoryResetProtectionPolicy.aidl | 19 ++ .../app/admin/FactoryResetProtectionPolicy.java | 237 +++++++++++++++++++++ .../android/app/admin/IDevicePolicyManager.aidl | 4 + .../stats/devicepolicy/device_policy_enums.proto | 1 + core/res/AndroidManifest.xml | 1 + .../server/PersistentDataBlockManagerInternal.java | 3 + .../android/server/PersistentDataBlockService.java | 5 + .../devicepolicy/DevicePolicyManagerService.java | 88 ++++++++ .../DevicePolicyManagerServiceTestable.java | 6 + .../devicepolicy/DevicePolicyManagerTest.java | 113 ++++++++++ .../FactoryResetProtectionPolicyTest.java | 150 +++++++++++++ .../server/devicepolicy/MockSystemServices.java | 3 + 15 files changed, 712 insertions(+) create mode 100644 core/java/android/app/admin/FactoryResetProtectionPolicy.aidl create mode 100644 core/java/android/app/admin/FactoryResetProtectionPolicy.java create mode 100644 services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java diff --git a/api/current.txt b/api/current.txt index 36567ecc1a72..05fed2390dc5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6756,6 +6756,7 @@ package android.app.admin { method @NonNull public java.util.List getDelegatedScopes(@Nullable android.content.ComponentName, @NonNull String); method public CharSequence getDeviceOwnerLockScreenInfo(); method public CharSequence getEndUserSessionMessage(@NonNull android.content.ComponentName); + method @Nullable public android.app.admin.FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(@Nullable android.content.ComponentName); method @Nullable public String getGlobalPrivateDnsHost(@NonNull android.content.ComponentName); method public int getGlobalPrivateDnsMode(@NonNull android.content.ComponentName); method @NonNull public java.util.List getInstalledCaCerts(@Nullable android.content.ComponentName); @@ -6874,6 +6875,7 @@ package android.app.admin { method public void setDelegatedScopes(@NonNull android.content.ComponentName, @NonNull String, @NonNull java.util.List); method public void setDeviceOwnerLockScreenInfo(@NonNull android.content.ComponentName, CharSequence); method public void setEndUserSessionMessage(@NonNull android.content.ComponentName, @Nullable CharSequence); + method public void setFactoryResetProtectionPolicy(@NonNull android.content.ComponentName, @Nullable android.app.admin.FactoryResetProtectionPolicy); method public int setGlobalPrivateDnsModeOpportunistic(@NonNull android.content.ComponentName); method @WorkerThread public int setGlobalPrivateDnsModeSpecifiedHost(@NonNull android.content.ComponentName, @NonNull String); method public void setGlobalSetting(@NonNull android.content.ComponentName, String, String); @@ -7108,6 +7110,21 @@ package android.app.admin { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public final class FactoryResetProtectionPolicy implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List getFactoryResetProtectionAccounts(); + method public boolean isFactoryResetProtectionDisabled(); + method public void writeToParcel(@NonNull android.os.Parcel, @Nullable int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static class FactoryResetProtectionPolicy.Builder { + ctor public FactoryResetProtectionPolicy.Builder(); + method @NonNull public android.app.admin.FactoryResetProtectionPolicy build(); + method @NonNull public android.app.admin.FactoryResetProtectionPolicy.Builder setFactoryResetProtectionAccounts(@NonNull java.util.List); + method @NonNull public android.app.admin.FactoryResetProtectionPolicy.Builder setFactoryResetProtectionDisabled(boolean); + } + public class FreezePeriod { ctor public FreezePeriod(java.time.MonthDay, java.time.MonthDay); method public java.time.MonthDay getEnd(); diff --git a/api/system-current.txt b/api/system-current.txt index 1146522dafea..d768c0228bdd 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -810,6 +810,7 @@ package android.app.admin { field public static final String ACCOUNT_FEATURE_DEVICE_OR_PROFILE_OWNER_DISALLOWED = "android.account.DEVICE_OR_PROFILE_OWNER_DISALLOWED"; field public static final String ACTION_PROVISION_FINALIZATION = "android.app.action.PROVISION_FINALIZATION"; field public static final String ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE = "android.app.action.PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE"; + field public static final String ACTION_RESET_PROTECTION_POLICY_CHANGED = "android.app.action.RESET_PROTECTION_POLICY_CHANGED"; field public static final String ACTION_SET_PROFILE_OWNER = "android.app.action.SET_PROFILE_OWNER"; field public static final String ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE"; field public static final String EXTRA_PROFILE_OWNER_NAME = "android.app.extra.PROFILE_OWNER_NAME"; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index f3028a10bb10..eaea226d1d0e 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1371,6 +1371,16 @@ public class DevicePolicyManager { public static final String ACTION_DEVICE_OWNER_CHANGED = "android.app.action.DEVICE_OWNER_CHANGED"; + /** + * Broadcast action: sent when the factory reset protection (FRP) policy is changed. + * + * @see #setFactoryResetProtectionPolicy + * @hide + */ + @SystemApi + public static final String ACTION_RESET_PROTECTION_POLICY_CHANGED = + "android.app.action.RESET_PROTECTION_POLICY_CHANGED"; + /** * The ComponentName of the administrator component. * @@ -4289,6 +4299,60 @@ public class DevicePolicyManager { } } + /** + * Callable by device owner or profile owner of an organization-owned device, to set a + * factory reset protection (FRP) policy. When a new policy is set, the system + * notifies the FRP management agent of a policy change by broadcasting + * {@code ACTION_RESET_PROTECTION_POLICY_CHANGED}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param policy the new FRP policy, or {@code null} to clear the current policy. + * @throws SecurityException if {@code admin} is not a device owner or a profile owner of + * an organization-owned device. + * @throws UnsupportedOperationException if factory reset protection is not + * supported on the device. + */ + public void setFactoryResetProtectionPolicy(@NonNull ComponentName admin, + @Nullable FactoryResetProtectionPolicy policy) { + throwIfParentInstance("setFactoryResetProtectionPolicy"); + if (mService != null) { + try { + mService.setFactoryResetProtectionPolicy(admin, policy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Callable by device owner or profile owner of an organization-owned device, to retrieve + * the current factory reset protection (FRP) policy set previously by + * {@link #setFactoryResetProtectionPolicy}. + *

+ * This method can also be called by the FRP management agent on device, in which case, + * it can pass {@code null} as the ComponentName. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with or + * {@code null} if called by the FRP management agent on device. + * @return The current FRP policy object or {@code null} if no policy is set. + * @throws SecurityException if {@code admin} is not a device owner, a profile owner of + * an organization-owned device or the FRP management agent. + * @throws UnsupportedOperationException if factory reset protection is not + * supported on the device. + */ + public @Nullable FactoryResetProtectionPolicy getFactoryResetProtectionPolicy( + @Nullable ComponentName admin) { + throwIfParentInstance("getFactoryResetProtectionPolicy"); + if (mService != null) { + try { + return mService.getFactoryResetProtectionPolicy(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + /** * Called by an application that is administering the device to set the * global proxy and exclusion list. diff --git a/core/java/android/app/admin/FactoryResetProtectionPolicy.aidl b/core/java/android/app/admin/FactoryResetProtectionPolicy.aidl new file mode 100644 index 000000000000..72e639a76d18 --- /dev/null +++ b/core/java/android/app/admin/FactoryResetProtectionPolicy.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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.app.admin; + +parcelable FactoryResetProtectionPolicy; diff --git a/core/java/android/app/admin/FactoryResetProtectionPolicy.java b/core/java/android/app/admin/FactoryResetProtectionPolicy.java new file mode 100644 index 000000000000..ed7477936f9c --- /dev/null +++ b/core/java/android/app/admin/FactoryResetProtectionPolicy.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2019 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.app.admin; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * The factory reset protection policy determines which accounts can unlock a device that + * has gone through untrusted factory reset. + *

+ * Only a device owner or profile owner of an organization-owned device can set a factory + * reset protection policy for the device by calling the {@code DevicePolicyManager} method + * {@link DevicePolicyManager#setFactoryResetProtectionPolicy(ComponentName, + * FactoryResetProtectionPolicy)}}. + * + * @see DevicePolicyManager#setFactoryResetProtectionPolicy + * @see DevicePolicyManager#getFactoryResetProtectionPolicy + */ +public final class FactoryResetProtectionPolicy implements Parcelable { + + private static final String LOG_TAG = "FactoryResetProtectionPolicy"; + + private static final String KEY_FACTORY_RESET_PROTECTION_ACCOUNT = + "factory_reset_protection_account"; + private static final String KEY_FACTORY_RESET_PROTECTION_DISABLED = + "factory_reset_protection_disabled"; + private static final String ATTR_VALUE = "value"; + + private final List mFactoryResetProtectionAccounts; + private final boolean mFactoryResetProtectionDisabled; + + private FactoryResetProtectionPolicy(List factoryResetProtectionAccounts, + boolean factoryResetProtectionDisabled) { + mFactoryResetProtectionAccounts = factoryResetProtectionAccounts; + mFactoryResetProtectionDisabled = factoryResetProtectionDisabled; + } + + /** + * Get the list of accounts that can provision a device which has been factory reset. + */ + public @NonNull List getFactoryResetProtectionAccounts() { + return mFactoryResetProtectionAccounts; + } + + /** + * Return whether factory reset protection for the device is disabled or not. + */ + public boolean isFactoryResetProtectionDisabled() { + return mFactoryResetProtectionDisabled; + } + + /** + * Builder class for {@link FactoryResetProtectionPolicy} objects. + */ + public static class Builder { + private List mFactoryResetProtectionAccounts; + private boolean mFactoryResetProtectionDisabled; + + /** + * Initialize a new Builder to construct a {@link FactoryResetProtectionPolicy}. + */ + public Builder() { + }; + + /** + * Sets which accounts can unlock a device that has been factory reset. + *

+ * Once set, the consumer unlock flow will be disabled and only accounts in this list + * can unlock factory reset protection after untrusted factory reset. + *

+ * It's up to the FRP management agent to interpret the {@code String} as account it + * supports. Please consult their relevant documentation for details. + * + * @param factoryResetProtectionAccounts list of accounts. + * @return the same Builder instance. + */ + @NonNull + public Builder setFactoryResetProtectionAccounts( + @NonNull List factoryResetProtectionAccounts) { + mFactoryResetProtectionAccounts = new ArrayList<>(factoryResetProtectionAccounts); + return this; + } + + /** + * Sets whether factory reset protection is disabled or not. + *

+ * Once disabled, factory reset protection will not kick in all together when the device + * goes through untrusted factory reset. This applies to both the consumer unlock flow and + * the admin account overrides via {@link #setFactoryResetProtectionAccounts} + * + * @param factoryResetProtectionDisabled Whether the policy is disabled or not. + * @return the same Builder instance. + */ + @NonNull + public Builder setFactoryResetProtectionDisabled(boolean factoryResetProtectionDisabled) { + mFactoryResetProtectionDisabled = factoryResetProtectionDisabled; + return this; + } + + /** + * Combines all of the attributes that have been set on this {@code Builder} + * + * @return a new {@link FactoryResetProtectionPolicy} object. + */ + @NonNull + public FactoryResetProtectionPolicy build() { + return new FactoryResetProtectionPolicy(mFactoryResetProtectionAccounts, + mFactoryResetProtectionDisabled); + } + } + + @Override + public String toString() { + return "FactoryResetProtectionPolicy{" + + "mFactoryResetProtectionAccounts=" + mFactoryResetProtectionAccounts + + ", mFactoryResetProtectionDisabled=" + mFactoryResetProtectionDisabled + + '}'; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, @Nullable int flags) { + int accountsCount = mFactoryResetProtectionAccounts.size(); + dest.writeInt(accountsCount); + for (String account: mFactoryResetProtectionAccounts) { + dest.writeString(account); + } + dest.writeBoolean(mFactoryResetProtectionDisabled); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator CREATOR = + new Creator() { + + @Override + public FactoryResetProtectionPolicy createFromParcel(Parcel in) { + List factoryResetProtectionAccounts = new ArrayList<>(); + int accountsCount = in.readInt(); + for (int i = 0; i < accountsCount; i++) { + factoryResetProtectionAccounts.add(in.readString()); + } + boolean factoryResetProtectionDisabled = in.readBoolean(); + + return new FactoryResetProtectionPolicy(factoryResetProtectionAccounts, + factoryResetProtectionDisabled); + } + + @Override + public FactoryResetProtectionPolicy[] newArray(int size) { + return new FactoryResetProtectionPolicy[size]; + } + }; + + /** + * Restore a previously saved FactoryResetProtectionPolicy from XML. + *

+ * No validation is required on the reconstructed policy since the XML was previously + * created by the system server from a validated policy. + * @hide + */ + @Nullable + public static FactoryResetProtectionPolicy readFromXml(@NonNull XmlPullParser parser) { + try { + boolean factoryResetProtectionDisabled = Boolean.parseBoolean( + parser.getAttributeValue(null, KEY_FACTORY_RESET_PROTECTION_DISABLED)); + + List factoryResetProtectionAccounts = new ArrayList<>(); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != END_DOCUMENT + && (type != END_TAG || parser.getDepth() > outerDepth)) { + if (type == END_TAG || type == TEXT) { + continue; + } + if (!parser.getName().equals(KEY_FACTORY_RESET_PROTECTION_ACCOUNT)) { + continue; + } + factoryResetProtectionAccounts.add( + parser.getAttributeValue(null, ATTR_VALUE)); + } + + return new FactoryResetProtectionPolicy(factoryResetProtectionAccounts, + factoryResetProtectionDisabled); + } catch (XmlPullParserException | IOException e) { + Log.w(LOG_TAG, "Reading from xml failed", e); + } + return null; + } + + /** + * @hide + */ + public void writeToXml(@NonNull XmlSerializer out) throws IOException { + out.attribute(null, KEY_FACTORY_RESET_PROTECTION_DISABLED, + Boolean.toString(mFactoryResetProtectionDisabled)); + for (String account : mFactoryResetProtectionAccounts) { + out.startTag(null, KEY_FACTORY_RESET_PROTECTION_ACCOUNT); + out.attribute(null, ATTR_VALUE, account); + out.endTag(null, KEY_FACTORY_RESET_PROTECTION_ACCOUNT); + } + } + +} diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 55dfe2fec4c9..92d6ba204772 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -24,6 +24,7 @@ import android.app.admin.StartInstallingUpdateCallback; import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; import android.app.admin.PasswordMetrics; +import android.app.admin.FactoryResetProtectionPolicy; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -104,6 +105,9 @@ interface IDevicePolicyManager { void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent); + void setFactoryResetProtectionPolicy(in ComponentName who, in FactoryResetProtectionPolicy policy); + FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(in ComponentName who); + ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList); ComponentName getGlobalProxyAdmin(int userHandle); void setRecommendedGlobalProxy(in ComponentName admin, in ProxyInfo proxyInfo); diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto index 9054d5462da5..0fca1d19c0e5 100644 --- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto +++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto @@ -154,4 +154,5 @@ enum EventId { SET_AUTO_TIME = 127; SET_AUTO_TIME_ZONE = 128; SET_PACKAGES_PROTECTED = 129; + SET_FACTORY_RESET_PROTECTION = 130; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 31faff6bd314..595796367cd3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -444,6 +444,7 @@ + diff --git a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java index 190fff1f669c..21fa9f9a9401 100644 --- a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java +++ b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java @@ -46,4 +46,7 @@ public interface PersistentDataBlockManagerInternal { /** Update the OEM unlock enabled bit, bypassing user restriction checks. */ void forceOemUnlockEnabled(boolean enabled); + + /** Retrieves the UID that can access the persistent data partition. */ + int getAllowedUid(); } diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java index 73c852083cfd..00d8b0f1bed4 100644 --- a/services/core/java/com/android/server/PersistentDataBlockService.java +++ b/services/core/java/com/android/server/PersistentDataBlockService.java @@ -680,6 +680,11 @@ public class PersistentDataBlockService extends SystemService { writeDataBuffer(getTestHarnessModeDataOffset(), ByteBuffer.allocate(size)); } + @Override + public int getAllowedUid() { + return mAllowedUid; + } + private void writeInternal(byte[] data, long offset, int dataLength) { checkArgument(data == null || data.length > 0, "data must be null or non-empty"); checkArgument( diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 13246ba9e587..d54fc0387de6 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -129,6 +129,7 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.DeviceStateCache; +import android.app.admin.FactoryResetProtectionPolicy; import android.app.admin.NetworkEvent; import android.app.admin.PasswordMetrics; import android.app.admin.PasswordPolicy; @@ -268,6 +269,7 @@ import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.PasswordValidationError; import com.android.server.LocalServices; import com.android.server.LockGuard; +import com.android.server.PersistentDataBlockManagerInternal; import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; import com.android.server.devicepolicy.DevicePolicyManagerService.ActiveAdmin.TrustAgentInfo; @@ -1006,6 +1008,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private static final String TAG_CROSS_PROFILE_CALENDAR_PACKAGES_NULL = "cross-profile-calendar-packages-null"; private static final String TAG_CROSS_PROFILE_PACKAGES = "cross-profile-packages"; + private static final String TAG_FACTORY_RESET_PROTECTION_POLICY = + "factory_reset_protection_policy"; DeviceAdminInfo info; @@ -1016,6 +1020,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @NonNull PasswordPolicy mPasswordPolicy = new PasswordPolicy(); + @Nullable + FactoryResetProtectionPolicy mFactoryResetProtectionPolicy = null; + static final long DEF_MAXIMUM_TIME_TO_UNLOCK = 0; long maximumTimeToUnlock = DEF_MAXIMUM_TIME_TO_UNLOCK; @@ -1351,6 +1358,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mCrossProfileCalendarPackages); } writePackageListToXml(out, TAG_CROSS_PROFILE_PACKAGES, mCrossProfilePackages); + if (mFactoryResetProtectionPolicy != null) { + out.startTag(null, TAG_FACTORY_RESET_PROTECTION_POLICY); + mFactoryResetProtectionPolicy.writeToXml(out); + out.endTag(null, TAG_FACTORY_RESET_PROTECTION_POLICY); + } } void writeTextToXml(XmlSerializer out, String tag, String text) throws IOException { @@ -1584,6 +1596,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mCrossProfileCalendarPackages = null; } else if (TAG_CROSS_PROFILE_PACKAGES.equals(tag)) { mCrossProfilePackages = readPackageList(parser, tag); + } else if (TAG_FACTORY_RESET_PROTECTION_POLICY.equals(tag)) { + mFactoryResetProtectionPolicy = FactoryResetProtectionPolicy.readFromXml( + parser); } else { Slog.w(LOG_TAG, "Unknown admin tag: " + tag); XmlUtils.skipCurrentTag(parser); @@ -2036,6 +2051,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return IAudioService.Stub.asInterface(ServiceManager.getService(Context.AUDIO_SERVICE)); } + PersistentDataBlockManagerInternal getPersistentDataBlockManagerInternal() { + return LocalServices.getService(PersistentDataBlockManagerInternal.class); + } + LockSettingsInternal getLockSettingsInternal() { return LocalServices.getService(LockSettingsInternal.class); } @@ -6735,6 +6754,67 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PROFILE_WIPED); } + @Override + public void setFactoryResetProtectionPolicy(ComponentName who, + @Nullable FactoryResetProtectionPolicy policy) { + if (!mHasFeature) { + return; + } + Preconditions.checkNotNull(who, "ComponentName is null"); + + final int frpManagementAgentUid = getFrpManagementAgentUidOrThrow(); + final int userId = mInjector.userHandleGetCallingUserId(); + synchronized (getLockObject()) { + ActiveAdmin admin = getActiveAdminForCallerLocked( + who, DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER); + admin.mFactoryResetProtectionPolicy = policy; + saveSettingsLocked(userId); + } + + mInjector.binderWithCleanCallingIdentity(() -> mContext.sendBroadcastAsUser( + new Intent(DevicePolicyManager.ACTION_RESET_PROTECTION_POLICY_CHANGED), + UserHandle.getUserHandleForUid(frpManagementAgentUid))); + + DevicePolicyEventLogger + .createEvent(DevicePolicyEnums.SET_FACTORY_RESET_PROTECTION) + .setAdmin(who) + .write(); + } + + @Override + public FactoryResetProtectionPolicy getFactoryResetProtectionPolicy( + @Nullable ComponentName who) { + if (!mHasFeature) { + return null; + } + + final int frpManagementAgentUid = getFrpManagementAgentUidOrThrow(); + ActiveAdmin admin; + synchronized (getLockObject()) { + if (who == null) { + if ((frpManagementAgentUid != mInjector.binderGetCallingUid())) { + throw new SecurityException( + "Must be called by the FRP management agent on device"); + } + admin = getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked( + UserHandle.getUserId(frpManagementAgentUid)); + } else { + admin = getActiveAdminForCallerLocked( + who, DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER); + } + } + return admin != null ? admin.mFactoryResetProtectionPolicy : null; + } + + private int getFrpManagementAgentUidOrThrow() { + PersistentDataBlockManagerInternal pdb = mInjector.getPersistentDataBlockManagerInternal(); + if ((pdb == null) || (pdb.getAllowedUid() == -1)) { + throw new UnsupportedOperationException( + "The persistent data block service is not supported on this device"); + } + return pdb.getAllowedUid(); + } + @Override public void getRemoveWarning(ComponentName comp, final RemoteCallback result, int userHandle) { if (!mHasFeature) { @@ -8131,6 +8211,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return null; } + ActiveAdmin getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(int userId) { + ActiveAdmin admin = getDeviceOwnerAdminLocked(); + if (admin == null) { + admin = getProfileOwnerOfOrganizationOwnedDeviceLocked(userId); + } + return admin; + } + @Override public void clearDeviceOwner(String packageName) { Objects.requireNonNull(packageName, "packageName is null"); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index ac555fda2204..3a8258be5f01 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -51,6 +51,7 @@ import androidx.annotation.NonNull; import com.android.internal.util.FunctionalUtils.ThrowingRunnable; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockSettingsInternal; +import com.android.server.PersistentDataBlockManagerInternal; import com.android.server.net.NetworkPolicyManagerInternal; import java.io.File; @@ -222,6 +223,11 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi return services.iaudioService; } + @Override + PersistentDataBlockManagerInternal getPersistentDataBlockManagerInternal() { + return services.persistentDataBlockManagerInternal; + } + @Override Looper getMyLooper() { return Looper.getMainLooper(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index a16e14f61a66..6d68446ae2b2 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -31,6 +31,8 @@ import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; import static com.android.internal.widget.LockPatternUtils.EscrowTokenStateChangeCallback; import static com.android.server.testutils.TestUtils.assertExpectException; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; @@ -62,6 +64,7 @@ import android.app.Notification; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; +import android.app.admin.FactoryResetProtectionPolicy; import android.app.admin.PasswordMetrics; import android.app.timedetector.ManualTimeSuggestion; import android.app.timezonedetector.ManualTimeZoneSuggestion; @@ -2022,6 +2025,116 @@ public class DevicePolicyManagerTest extends DpmTestBase { ); } + public void testSetFactoryResetProtectionPolicyWithDO() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + + when(getServices().persistentDataBlockManagerInternal.getAllowedUid()).thenReturn( + DpmMockContext.CALLER_UID); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionAccounts(new ArrayList<>()) + .setFactoryResetProtectionDisabled(true) + .build(); + dpm.setFactoryResetProtectionPolicy(admin1, policy); + + FactoryResetProtectionPolicy result = dpm.getFactoryResetProtectionPolicy(admin1); + assertThat(result).isEqualTo(policy); + assertPoliciesAreEqual(policy, result); + + verify(mContext.spiedContext).sendBroadcastAsUser( + MockUtils.checkIntentAction( + DevicePolicyManager.ACTION_RESET_PROTECTION_POLICY_CHANGED), + MockUtils.checkUserHandle(DpmMockContext.CALLER_USER_HANDLE)); + } + + public void testSetFactoryResetProtectionPolicyFailWithPO() throws Exception { + setupProfileOwner(); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionDisabled(true) + .build(); + + assertExpectException(SecurityException.class, null, + () -> dpm.setFactoryResetProtectionPolicy(admin1, policy)); + } + + public void testSetFactoryResetProtectionPolicyWithPOOfOrganizationOwnedDevice() + throws Exception { + setupProfileOwner(); + configureProfileOwnerOfOrgOwnedDevice(admin1, DpmMockContext.CALLER_USER_HANDLE); + + when(getServices().persistentDataBlockManagerInternal.getAllowedUid()).thenReturn( + DpmMockContext.CALLER_UID); + + List accounts = new ArrayList<>(); + accounts.add("Account 1"); + accounts.add("Account 2"); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionAccounts(accounts) + .build(); + + dpm.setFactoryResetProtectionPolicy(admin1, policy); + + FactoryResetProtectionPolicy result = dpm.getFactoryResetProtectionPolicy(admin1); + assertThat(result).isEqualTo(policy); + assertPoliciesAreEqual(policy, result); + + verify(mContext.spiedContext, times(2)).sendBroadcastAsUser( + MockUtils.checkIntentAction( + DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + MockUtils.checkUserHandle(DpmMockContext.CALLER_USER_HANDLE)); + verify(mContext.spiedContext).sendBroadcastAsUser( + MockUtils.checkIntentAction( + DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED), + MockUtils.checkUserHandle(DpmMockContext.CALLER_USER_HANDLE)); + verify(mContext.spiedContext).sendBroadcastAsUser( + MockUtils.checkIntentAction( + DevicePolicyManager.ACTION_RESET_PROTECTION_POLICY_CHANGED), + MockUtils.checkUserHandle(DpmMockContext.CALLER_USER_HANDLE)); + } + + public void testGetFactoryResetProtectionPolicyWithFrpManagementAgent() + throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; + setupDeviceOwner(); + when(getServices().persistentDataBlockManagerInternal.getAllowedUid()).thenReturn( + DpmMockContext.CALLER_UID); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionAccounts(new ArrayList<>()) + .setFactoryResetProtectionDisabled(true) + .build(); + + dpm.setFactoryResetProtectionPolicy(admin1, policy); + + mContext.callerPermissions.add(permission.MANAGE_DEVICE_ADMINS); + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.setActiveAdmin(admin1, /*replace=*/ false); + FactoryResetProtectionPolicy result = dpm.getFactoryResetProtectionPolicy(null); + assertThat(result).isEqualTo(policy); + assertPoliciesAreEqual(policy, result); + + verify(mContext.spiedContext).sendBroadcastAsUser( + MockUtils.checkIntentAction( + DevicePolicyManager.ACTION_RESET_PROTECTION_POLICY_CHANGED), + MockUtils.checkUserHandle(DpmMockContext.CALLER_USER_HANDLE)); + } + + private void assertPoliciesAreEqual(FactoryResetProtectionPolicy expectedPolicy, + FactoryResetProtectionPolicy actualPolicy) { + assertThat(actualPolicy.isFactoryResetProtectionDisabled()).isEqualTo( + expectedPolicy.isFactoryResetProtectionDisabled()); + assertAccountsAreEqual(expectedPolicy.getFactoryResetProtectionAccounts(), + actualPolicy.getFactoryResetProtectionAccounts()); + } + + private void assertAccountsAreEqual(List expectedAccounts, + List actualAccounts) { + assertThat(actualAccounts).containsExactlyElementsIn(expectedAccounts); + } + public void testGetMacAddress() throws Exception { mContext.callerPermissions.add(permission.MANAGE_DEVICE_ADMINS); mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java new file mode 100644 index 000000000000..bc853c693b3a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2019 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.devicepolicy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.admin.FactoryResetProtectionPolicy; +import android.os.Parcel; +import android.util.Xml; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.FastXmlSerializer; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link android.app.admin.FactoryResetProtectionPolicy}. + * + * atest com.android.server.devicepolicy.FactoryResetProtectionPolicyTest + */ +@RunWith(AndroidJUnit4.class) +public class FactoryResetProtectionPolicyTest { + + private static final String TAG_FACTORY_RESET_PROTECTION_POLICY = + "factory_reset_protection_policy"; + + @Test + public void testNonDefaultFactoryResetProtectionPolicyObject() throws Exception { + List accounts = new ArrayList<>(); + accounts.add("Account 1"); + accounts.add("Account 2"); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionAccounts(accounts) + .setFactoryResetProtectionDisabled(true) + .build(); + + testParcelAndUnparcel(policy); + testSerializationAndDeserialization(policy); + } + + @Test + public void testInvalidXmlFactoryResetProtectionPolicyObject() throws Exception { + List accounts = new ArrayList<>(); + accounts.add("Account 1"); + accounts.add("Account 2"); + + FactoryResetProtectionPolicy policy = new FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionAccounts(accounts) + .setFactoryResetProtectionDisabled(true) + .build(); + + testParcelAndUnparcel(policy); + testInvalidXmlSerializationAndDeserialization(policy); + } + + private void testParcelAndUnparcel(FactoryResetProtectionPolicy policy) { + Parcel parcel = Parcel.obtain(); + policy.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + FactoryResetProtectionPolicy actualPolicy = + FactoryResetProtectionPolicy.CREATOR.createFromParcel(parcel); + assertPoliciesAreEqual(policy, actualPolicy); + parcel.recycle(); + } + + private void testSerializationAndDeserialization(FactoryResetProtectionPolicy policy) + throws Exception { + ByteArrayOutputStream outStream = serialize(policy); + ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray()); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new InputStreamReader(inStream)); + assertEquals(XmlPullParser.START_TAG, parser.next()); + + assertPoliciesAreEqual(policy, policy.readFromXml(parser)); + } + + private void testInvalidXmlSerializationAndDeserialization(FactoryResetProtectionPolicy policy) + throws Exception { + ByteArrayOutputStream outStream = serialize(policy); + ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray()); + XmlPullParser parser = mock(XmlPullParser.class); + when(parser.next()).thenThrow(XmlPullParserException.class); + parser.setInput(new InputStreamReader(inStream)); + + // If deserialization fails, then null is returned. + assertNull(policy.readFromXml(parser)); + } + + private ByteArrayOutputStream serialize(FactoryResetProtectionPolicy policy) + throws IOException { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + final XmlSerializer outXml = new FastXmlSerializer(); + outXml.setOutput(outStream, StandardCharsets.UTF_8.name()); + outXml.startDocument(null, true); + outXml.startTag(null, TAG_FACTORY_RESET_PROTECTION_POLICY); + policy.writeToXml(outXml); + outXml.endTag(null, TAG_FACTORY_RESET_PROTECTION_POLICY); + outXml.endDocument(); + outXml.flush(); + return outStream; + } + + private void assertPoliciesAreEqual(FactoryResetProtectionPolicy expectedPolicy, + FactoryResetProtectionPolicy actualPolicy) { + assertEquals(expectedPolicy.isFactoryResetProtectionDisabled(), + actualPolicy.isFactoryResetProtectionDisabled()); + assertAccountsAreEqual(expectedPolicy.getFactoryResetProtectionAccounts(), + actualPolicy.getFactoryResetProtectionAccounts()); + } + + private void assertAccountsAreEqual(List expectedAccounts, + List actualAccounts) { + assertEquals(expectedAccounts.size(), actualAccounts.size()); + for (int i = 0; i < expectedAccounts.size(); i++) { + assertEquals(expectedAccounts.get(i), actualAccounts.get(i)); + } + } + +} diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 919a3f6d7d1b..6c2c1446a459 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -68,6 +68,7 @@ import android.view.IWindowManager; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockSettingsInternal; +import com.android.server.PersistentDataBlockManagerInternal; import com.android.server.net.NetworkPolicyManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; @@ -117,6 +118,7 @@ public class MockSystemServices { public final TimeDetector timeDetector; public final TimeZoneDetector timeZoneDetector; public final KeyChain.KeyChainConnection keyChainConnection; + public final PersistentDataBlockManagerInternal persistentDataBlockManagerInternal; /** Note this is a partial mock, not a real mock. */ public final PackageManager packageManager; public final BuildMock buildMock = new BuildMock(); @@ -160,6 +162,7 @@ public class MockSystemServices { timeDetector = mock(TimeDetector.class); timeZoneDetector = mock(TimeZoneDetector.class); keyChainConnection = mock(KeyChain.KeyChainConnection.class, RETURNS_DEEP_STUBS); + persistentDataBlockManagerInternal = mock(PersistentDataBlockManagerInternal.class); // Package manager is huge, so we use a partial mock instead. packageManager = spy(realContext.getPackageManager()); -- cgit v1.2.3-59-g8ed1b