diff --git a/Android.bp b/Android.bp index 20ca1b7c1020..709929139fad 100644 --- a/Android.bp +++ b/Android.bp @@ -584,6 +584,7 @@ java_library { "android.security.apc-java", "android.security.authorization-java", "android.security.usermanager-java", + "android.security.vpnprofilestore-java", "android.system.keystore2-V1-java", "android.system.suspend.control.internal-java", "cameraprotosnano", diff --git a/core/java/android/net/Ikev2VpnProfile.java b/core/java/android/net/Ikev2VpnProfile.java index 183f500572bd..cc1312bac180 100644 --- a/core/java/android/net/Ikev2VpnProfile.java +++ b/core/java/android/net/Ikev2VpnProfile.java @@ -24,10 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.content.pm.PackageManager; -import android.os.Process; import android.security.Credentials; -import android.security.KeyStore; -import android.security.keystore.AndroidKeyStoreProvider; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.net.VpnProfile; @@ -35,7 +32,9 @@ import com.android.internal.net.VpnProfile; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyFactory; +import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; @@ -66,6 +65,7 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile { /** Prefix for when a Private Key is stored directly in the profile @hide */ public static final String PREFIX_INLINE = "INLINE:"; + private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; private static final String MISSING_PARAM_MSG_TMPL = "Required parameter was not provided: %s"; private static final String EMPTY_CERT = ""; @@ -430,32 +430,31 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile { return profile; } - /** - * Constructs a Ikev2VpnProfile from an internal-use VpnProfile instance. - * - *

Redundant authentication information (not related to profile type) will be discarded. - * - * @hide - */ - @NonNull - public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile) - throws IOException, GeneralSecurityException { - return fromVpnProfile(profile, null); + private static PrivateKey getPrivateKeyFromAndroidKeystore(String alias) { + try { + final KeyStore keystore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER); + keystore.load(null); + final Key key = keystore.getKey(alias, null); + if (!(key instanceof PrivateKey)) { + throw new IllegalStateException( + "Unexpected key type returned from android keystore."); + } + return (PrivateKey) key; + } catch (Exception e) { + throw new IllegalStateException("Failed to load key from android keystore.", e); + } } /** * Builds the Ikev2VpnProfile from the given profile. * * @param profile the source VpnProfile to build from - * @param keyStore the Android Keystore instance to use to retrieve the private key, or null if - * the private key is PEM-encoded into the profile. * @return The IKEv2/IPsec VPN profile * @hide */ @NonNull - public static Ikev2VpnProfile fromVpnProfile( - @NonNull VpnProfile profile, @Nullable KeyStore keyStore) - throws IOException, GeneralSecurityException { + public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile) + throws GeneralSecurityException { final Builder builder = new Builder(profile.server, profile.ipsecIdentifier); builder.setProxy(profile.proxy); builder.setAllowedAlgorithms(profile.getAllowedAlgorithms()); @@ -479,12 +478,9 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile { case TYPE_IKEV2_IPSEC_RSA: final PrivateKey key; if (profile.ipsecSecret.startsWith(PREFIX_KEYSTORE_ALIAS)) { - Objects.requireNonNull(keyStore, "Missing Keystore for aliased PrivateKey"); - final String alias = profile.ipsecSecret.substring(PREFIX_KEYSTORE_ALIAS.length()); - key = AndroidKeyStoreProvider.loadAndroidKeyStorePrivateKeyFromKeystore( - keyStore, alias, Process.myUid()); + key = getPrivateKeyFromAndroidKeystore(alias); } else if (profile.ipsecSecret.startsWith(PREFIX_INLINE)) { key = getPrivateKey(profile.ipsecSecret.substring(PREFIX_INLINE.length())); } else { diff --git a/keystore/java/android/security/LegacyVpnProfileStore.java b/keystore/java/android/security/LegacyVpnProfileStore.java new file mode 100644 index 000000000000..41cfb2707fcf --- /dev/null +++ b/keystore/java/android/security/LegacyVpnProfileStore.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security; + +import android.annotation.NonNull; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; +import android.security.keystore.AndroidKeyStoreProvider; +import android.security.vpnprofilestore.IVpnProfileStore; +import android.util.Log; + +/** + * @hide This class allows legacy VPN access to its profiles that were stored in Keystore. + * The storage of unstructured blobs in Android Keystore is going away, because there is no + * architectural or security benefit of storing profiles in keystore over storing them + * in the file system. This class allows access to the blobs that still exist in keystore. + * And it stores new blob in a database that is still owned by Android Keystore. + */ +public class LegacyVpnProfileStore { + private static final String TAG = "LegacyVpnProfileStore"; + + public static final int SYSTEM_ERROR = IVpnProfileStore.ERROR_SYSTEM_ERROR; + public static final int PROFILE_NOT_FOUND = IVpnProfileStore.ERROR_PROFILE_NOT_FOUND; + + private static final String VPN_PROFILE_STORE_SERVICE_NAME = "android.security.vpnprofilestore"; + + private static IVpnProfileStore getService() { + return IVpnProfileStore.Stub.asInterface( + ServiceManager.checkService(VPN_PROFILE_STORE_SERVICE_NAME)); + } + + /** + * Stores the profile under the alias in the profile database. Existing profiles by the + * same name will be replaced. + * @param alias The name of the profile + * @param profile The profile. + * @return true if the profile was successfully added. False otherwise. + * @hide + */ + public static boolean put(@NonNull String alias, @NonNull byte[] profile) { + try { + if (AndroidKeyStoreProvider.isKeystore2Enabled()) { + getService().put(alias, profile); + return true; + } else { + return KeyStore.getInstance().put( + alias, profile, KeyStore.UID_SELF, 0); + } + } catch (Exception e) { + Log.e(TAG, "Failed to put vpn profile.", e); + return false; + } + } + + /** + * Retrieves a profile by the name alias from the profile database. + * @param alias Name of the profile to retrieve. + * @return The unstructured blob, that is the profile that was stored using + * LegacyVpnProfileStore#put or with + * android.security.Keystore.put(Credentials.VPN + alias). + * Returns null if no profile was found. + * @hide + */ + public static byte[] get(@NonNull String alias) { + try { + if (AndroidKeyStoreProvider.isKeystore2Enabled()) { + return getService().get(alias); + } else { + return KeyStore.getInstance().get(alias, true /* suppressKeyNotFoundWarning */); + } + } catch (ServiceSpecificException e) { + if (e.errorCode != PROFILE_NOT_FOUND) { + Log.e(TAG, "Failed to get vpn profile.", e); + } + } catch (Exception e) { + Log.e(TAG, "Failed to get vpn profile.", e); + } + return null; + } + + /** + * Removes a profile by the name alias from the profile database. + * @param alias Name of the profile to be removed. + * @return True if a profile was removed. False if no such profile was found. + * @hide + */ + public static boolean remove(@NonNull String alias) { + try { + if (AndroidKeyStoreProvider.isKeystore2Enabled()) { + getService().remove(alias); + return true; + } else { + return KeyStore.getInstance().delete(alias); + } + } catch (ServiceSpecificException e) { + if (e.errorCode != PROFILE_NOT_FOUND) { + Log.e(TAG, "Failed to remove vpn profile.", e); + } + } catch (Exception e) { + Log.e(TAG, "Failed to remove vpn profile.", e); + } + return false; + } + + /** + * Lists the vpn profiles stored in the database. + * @return An array of strings representing the aliases stored in the profile database. + * The return value may be empty but never null. + * @hide + */ + public static @NonNull String[] list(@NonNull String prefix) { + try { + if (AndroidKeyStoreProvider.isKeystore2Enabled()) { + final String[] aliases = getService().list(prefix); + for (int i = 0; i < aliases.length; ++i) { + aliases[i] = aliases[i].substring(prefix.length()); + } + return aliases; + } else { + final String[] result = KeyStore.getInstance().list(prefix); + return result != null ? result : new String[0]; + } + } catch (Exception e) { + Log.e(TAG, "Failed to list vpn profiles.", e); + } + return new String[0]; + } +} diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java index 5d89bf1b1d82..56aabc208027 100644 --- a/services/core/java/com/android/server/VpnManagerService.java +++ b/services/core/java/com/android/server/VpnManagerService.java @@ -47,7 +47,6 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.security.Credentials; -import android.security.KeyStore; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; @@ -60,6 +59,7 @@ import com.android.internal.net.VpnProfile; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.VpnProfileStore; import com.android.server.net.LockdownVpnTracker; import java.io.FileDescriptor; @@ -83,7 +83,7 @@ public class VpnManagerService extends IVpnManager.Stub { private final Dependencies mDeps; private final ConnectivityManager mCm; - private final KeyStore mKeyStore; + private final VpnProfileStore mVpnProfileStore; private final INetworkManagementService mNMS; private final INetd mNetd; private final UserManager mUserManager; @@ -114,9 +114,9 @@ public class VpnManagerService extends IVpnManager.Stub { return new HandlerThread("VpnManagerService"); } - /** Returns the KeyStore instance to be used by this class. */ - public KeyStore getKeyStore() { - return KeyStore.getInstance(); + /** Return the VpnProfileStore to be used by this class */ + public VpnProfileStore getVpnProfileStore() { + return new VpnProfileStore(); } public INetd getNetd() { @@ -135,7 +135,7 @@ public class VpnManagerService extends IVpnManager.Stub { mHandlerThread = mDeps.makeHandlerThread(); mHandlerThread.start(); mHandler = mHandlerThread.getThreadHandler(); - mKeyStore = mDeps.getKeyStore(); + mVpnProfileStore = mDeps.getVpnProfileStore(); mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); mCm = mContext.getSystemService(ConnectivityManager.class); mNMS = mDeps.getINetworkManagementService(); @@ -289,7 +289,7 @@ public class VpnManagerService extends IVpnManager.Stub { public boolean provisionVpnProfile(@NonNull VpnProfile profile, @NonNull String packageName) { final int user = UserHandle.getUserId(mDeps.getCallingUid()); synchronized (mVpns) { - return mVpns.get(user).provisionVpnProfile(packageName, profile, mKeyStore); + return mVpns.get(user).provisionVpnProfile(packageName, profile); } } @@ -307,7 +307,7 @@ public class VpnManagerService extends IVpnManager.Stub { public void deleteVpnProfile(@NonNull String packageName) { final int user = UserHandle.getUserId(mDeps.getCallingUid()); synchronized (mVpns) { - mVpns.get(user).deleteVpnProfile(packageName, mKeyStore); + mVpns.get(user).deleteVpnProfile(packageName); } } @@ -325,7 +325,7 @@ public class VpnManagerService extends IVpnManager.Stub { final int user = UserHandle.getUserId(mDeps.getCallingUid()); synchronized (mVpns) { throwIfLockdownEnabled(); - mVpns.get(user).startVpnProfile(packageName, mKeyStore); + mVpns.get(user).startVpnProfile(packageName); } } @@ -358,7 +358,7 @@ public class VpnManagerService extends IVpnManager.Stub { } synchronized (mVpns) { throwIfLockdownEnabled(); - mVpns.get(user).startLegacyVpn(profile, mKeyStore, null /* underlying */, egress); + mVpns.get(user).startLegacyVpn(profile, null /* underlying */, egress); } } @@ -396,7 +396,7 @@ public class VpnManagerService extends IVpnManager.Stub { } private boolean isLockdownVpnEnabled() { - return mKeyStore.contains(Credentials.LOCKDOWN_VPN); + return mVpnProfileStore.get(Credentials.LOCKDOWN_VPN) != null; } @Override @@ -417,14 +417,14 @@ public class VpnManagerService extends IVpnManager.Stub { return true; } - byte[] profileTag = mKeyStore.get(Credentials.LOCKDOWN_VPN); + byte[] profileTag = mVpnProfileStore.get(Credentials.LOCKDOWN_VPN); if (profileTag == null) { loge("Lockdown VPN configured but cannot be read from keystore"); return false; } String profileName = new String(profileTag); final VpnProfile profile = VpnProfile.decode( - profileName, mKeyStore.get(Credentials.VPN + profileName)); + profileName, mVpnProfileStore.get(Credentials.VPN + profileName)); if (profile == null) { loge("Lockdown VPN configured invalid profile " + profileName); setLockdownTracker(null); @@ -437,7 +437,7 @@ public class VpnManagerService extends IVpnManager.Stub { return false; } setLockdownTracker( - new LockdownVpnTracker(mContext, mHandler, mKeyStore, vpn, profile)); + new LockdownVpnTracker(mContext, mHandler, vpn, profile)); } return true; @@ -495,7 +495,7 @@ public class VpnManagerService extends IVpnManager.Stub { return false; } - return vpn.startAlwaysOnVpn(mKeyStore); + return vpn.startAlwaysOnVpn(); } } @@ -510,7 +510,7 @@ public class VpnManagerService extends IVpnManager.Stub { logw("User " + userId + " has no Vpn configuration"); return false; } - return vpn.isAlwaysOnPackageSupported(packageName, mKeyStore); + return vpn.isAlwaysOnPackageSupported(packageName); } } @@ -531,11 +531,11 @@ public class VpnManagerService extends IVpnManager.Stub { logw("User " + userId + " has no Vpn configuration"); return false; } - if (!vpn.setAlwaysOnPackage(packageName, lockdown, lockdownAllowlist, mKeyStore)) { + if (!vpn.setAlwaysOnPackage(packageName, lockdown, lockdownAllowlist)) { return false; } if (!startAlwaysOnVpn(userId)) { - vpn.setAlwaysOnPackage(null, false, null, mKeyStore); + vpn.setAlwaysOnPackage(null, false, null); return false; } } @@ -705,7 +705,8 @@ public class VpnManagerService extends IVpnManager.Stub { loge("Starting user already has a VPN"); return; } - userVpn = new Vpn(mHandler.getLooper(), mContext, mNMS, mNetd, userId, mKeyStore); + userVpn = new Vpn(mHandler.getLooper(), mContext, mNMS, mNetd, userId, + new VpnProfileStore()); mVpns.put(userId, userVpn); if (mUserManager.getUserInfo(userId).isPrimary() && isLockdownVpnEnabled()) { updateLockdownVpn(); @@ -777,7 +778,7 @@ public class VpnManagerService extends IVpnManager.Stub { if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) { log("Restarting always-on VPN package " + packageName + " for user " + userId); - vpn.startAlwaysOnVpn(mKeyStore); + vpn.startAlwaysOnVpn(); } } } @@ -798,7 +799,7 @@ public class VpnManagerService extends IVpnManager.Stub { if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) { log("Removing always-on VPN package " + packageName + " for user " + userId); - vpn.setAlwaysOnPackage(null, false, null, mKeyStore); + vpn.setAlwaysOnPackage(null, false, null); } } } @@ -843,7 +844,7 @@ public class VpnManagerService extends IVpnManager.Stub { if (mLockdownEnabled && userId == UserHandle.USER_SYSTEM) { final long ident = Binder.clearCallingIdentity(); try { - mKeyStore.delete(Credentials.LOCKDOWN_VPN); + mVpnProfileStore.remove(Credentials.LOCKDOWN_VPN); mLockdownEnabled = false; setLockdownTracker(null); } finally { diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 67f495a455fb..2e61ae1b3483 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -101,7 +101,12 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.security.Credentials; -import android.security.KeyStore; +import android.security.KeyStore2; +import android.security.keystore.AndroidKeyStoreProvider; +import android.security.keystore.KeyProperties; +import android.system.keystore2.Domain; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyPermission; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -132,6 +137,12 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -157,6 +168,7 @@ public class Vpn { private static final String TAG = "Vpn"; private static final String VPN_PROVIDER_NAME_BASE = "VpnNetworkProvider:"; private static final boolean LOGD = true; + private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; // Length of time (in milliseconds) that an app hosting an always-on VPN is placed on // the device idle allowlist during service launch and VPN bootstrap. @@ -216,6 +228,13 @@ public class Vpn { private final Ikev2SessionCreator mIkev2SessionCreator; private final UserManager mUserManager; + private final VpnProfileStore mVpnProfileStore; + + @VisibleForTesting + VpnProfileStore getVpnProfileStore() { + return mVpnProfileStore; + } + /** * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This * only applies to {@link VpnService} connections. @@ -393,24 +412,25 @@ public class Vpn { } public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd, - @UserIdInt int userId, @NonNull KeyStore keyStore) { - this(looper, context, new Dependencies(), netService, netd, userId, keyStore, + @UserIdInt int userId, VpnProfileStore vpnProfileStore) { + this(looper, context, new Dependencies(), netService, netd, userId, vpnProfileStore, new SystemServices(context), new Ikev2SessionCreator()); } @VisibleForTesting public Vpn(Looper looper, Context context, Dependencies deps, INetworkManagementService netService, INetd netd, @UserIdInt int userId, - @NonNull KeyStore keyStore) { - this(looper, context, deps, netService, netd, userId, keyStore, + VpnProfileStore vpnProfileStore) { + this(looper, context, deps, netService, netd, userId, vpnProfileStore, new SystemServices(context), new Ikev2SessionCreator()); } @VisibleForTesting protected Vpn(Looper looper, Context context, Dependencies deps, INetworkManagementService netService, INetd netd, - int userId, @NonNull KeyStore keyStore, SystemServices systemServices, + int userId, VpnProfileStore vpnProfileStore, SystemServices systemServices, Ikev2SessionCreator ikev2SessionCreator) { + mVpnProfileStore = vpnProfileStore; mContext = context; mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); mUserIdContext = context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); @@ -446,7 +466,7 @@ public class Vpn { mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE)); - loadAlwaysOnPackage(keyStore); + loadAlwaysOnPackage(); } /** @@ -567,11 +587,9 @@ public class Vpn { * * * @param packageName the canonical package name of the VPN app - * @param keyStore the keystore instance to use for checking if the app has a Platform VPN - * profile installed. * @return {@code true} if and only if the VPN app exists and supports always-on mode */ - public boolean isAlwaysOnPackageSupported(String packageName, @NonNull KeyStore keyStore) { + public boolean isAlwaysOnPackageSupported(String packageName) { enforceSettingsPermission(); if (packageName == null) { @@ -580,7 +598,7 @@ public class Vpn { final long oldId = Binder.clearCallingIdentity(); try { - if (getVpnProfilePrivileged(packageName, keyStore) != null) { + if (getVpnProfilePrivileged(packageName) != null) { return true; } } finally { @@ -632,17 +650,15 @@ public class Vpn { * @param packageName the package to designate as always-on VPN supplier. * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting. * @param lockdownAllowlist packages to be allowed from lockdown. - * @param keyStore the Keystore instance to use for checking of PlatformVpnProfile(s) * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ public synchronized boolean setAlwaysOnPackage( @Nullable String packageName, boolean lockdown, - @Nullable List lockdownAllowlist, - @NonNull KeyStore keyStore) { + @Nullable List lockdownAllowlist) { enforceControlPermissionOrInternalCaller(); - if (setAlwaysOnPackageInternal(packageName, lockdown, lockdownAllowlist, keyStore)) { + if (setAlwaysOnPackageInternal(packageName, lockdown, lockdownAllowlist)) { saveAlwaysOnPackage(); return true; } @@ -659,13 +675,12 @@ public class Vpn { * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting. * @param lockdownAllowlist packages to be allowed to bypass lockdown. This is only used if * {@code lockdown} is {@code true}. Packages must not contain commas. - * @param keyStore the system keystore instance to check for profiles * @return {@code true} if the package has been set as always-on, {@code false} otherwise. */ @GuardedBy("this") private boolean setAlwaysOnPackageInternal( @Nullable String packageName, boolean lockdown, - @Nullable List lockdownAllowlist, @NonNull KeyStore keyStore) { + @Nullable List lockdownAllowlist) { if (VpnConfig.LEGACY_VPN.equals(packageName)) { Log.w(TAG, "Not setting legacy VPN \"" + packageName + "\" as always-on."); return false; @@ -684,7 +699,7 @@ public class Vpn { final VpnProfile profile; final long oldId = Binder.clearCallingIdentity(); try { - profile = getVpnProfilePrivileged(packageName, keyStore); + profile = getVpnProfilePrivileged(packageName); } finally { Binder.restoreCallingIdentity(oldId); } @@ -759,7 +774,7 @@ public class Vpn { /** Load the always-on package and lockdown config from Settings. */ @GuardedBy("this") - private void loadAlwaysOnPackage(@NonNull KeyStore keyStore) { + private void loadAlwaysOnPackage() { final long token = Binder.clearCallingIdentity(); try { final String alwaysOnPackage = mSystemServices.settingsSecureGetStringForUser( @@ -771,7 +786,7 @@ public class Vpn { final List allowedPackages = TextUtils.isEmpty(allowlistString) ? Collections.emptyList() : Arrays.asList(allowlistString.split(",")); setAlwaysOnPackageInternal( - alwaysOnPackage, alwaysOnLockdown, allowedPackages, keyStore); + alwaysOnPackage, alwaysOnLockdown, allowedPackages); } finally { Binder.restoreCallingIdentity(token); } @@ -780,11 +795,10 @@ public class Vpn { /** * Starts the currently selected always-on VPN * - * @param keyStore the keyStore instance for looking up PlatformVpnProfile(s) * @return {@code true} if the service was started, the service was already connected, or there * was no always-on VPN to start. {@code false} otherwise. */ - public boolean startAlwaysOnVpn(@NonNull KeyStore keyStore) { + public boolean startAlwaysOnVpn() { final String alwaysOnPackage; synchronized (this) { alwaysOnPackage = getAlwaysOnPackage(); @@ -793,8 +807,8 @@ public class Vpn { return true; } // Remove always-on VPN if it's not supported. - if (!isAlwaysOnPackageSupported(alwaysOnPackage, keyStore)) { - setAlwaysOnPackage(null, false, null, keyStore); + if (!isAlwaysOnPackageSupported(alwaysOnPackage)) { + setAlwaysOnPackage(null, false, null); return false; } // Skip if the service is already established. This isn't bulletproof: it's not bound @@ -808,10 +822,9 @@ public class Vpn { final long oldId = Binder.clearCallingIdentity(); try { // Prefer VPN profiles, if any exist. - VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage, keyStore); + VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage); if (profile != null) { - startVpnProfilePrivileged(profile, alwaysOnPackage, - null /* keyStore for private key retrieval - unneeded */); + startVpnProfilePrivileged(profile, alwaysOnPackage); // If the above startVpnProfilePrivileged() call returns, the Ikev2VpnProfile was // correctly parsed, and the VPN has started running in a different thread. The only @@ -2013,27 +2026,83 @@ public class Vpn { * secondary thread to perform connection work, returning quickly. * * Should only be called to respond to Binder requests as this enforces caller permission. Use - * {@link #startLegacyVpnPrivileged(VpnProfile, KeyStore, Network, LinkProperties)} to skip the + * {@link #startLegacyVpnPrivileged(VpnProfile, Network, LinkProperties)} to skip the * permission check only when the caller is trusted (or the call is initiated by the system). */ - public void startLegacyVpn(VpnProfile profile, KeyStore keyStore, @Nullable Network underlying, + public void startLegacyVpn(VpnProfile profile, @Nullable Network underlying, LinkProperties egress) { enforceControlPermission(); final long token = Binder.clearCallingIdentity(); try { - startLegacyVpnPrivileged(profile, keyStore, underlying, egress); + startLegacyVpnPrivileged(profile, underlying, egress); } finally { Binder.restoreCallingIdentity(token); } } + private String makeKeystoreEngineGrantString(String alias) { + if (alias == null) { + return null; + } + // If Keystore 2.0 is not enabled the legacy private key prefix is used. + if (!AndroidKeyStoreProvider.isKeystore2Enabled()) { + return Credentials.USER_PRIVATE_KEY + alias; + } + final KeyStore2 keystore2 = KeyStore2.getInstance(); + + KeyDescriptor key = new KeyDescriptor(); + key.domain = Domain.APP; + key.nspace = KeyProperties.NAMESPACE_APPLICATION; + key.alias = alias; + key.blob = null; + + final int grantAccessVector = KeyPermission.USE | KeyPermission.GET_INFO; + + try { + // The native vpn daemon is running as VPN_UID. This tells Keystore 2.0 + // to allow a process running with this UID to access the key designated by + // the KeyDescriptor `key`. `grant` returns a new KeyDescriptor with a grant + // identifier. This identifier needs to be communicated to the vpn daemon. + key = keystore2.grant(key, android.os.Process.VPN_UID, grantAccessVector); + } catch (android.security.KeyStoreException e) { + Log.e(TAG, "Failed to get grant for keystore key.", e); + throw new IllegalStateException("Failed to get grant for keystore key.", e); + } + + // Turn the grant identifier into a string as understood by the keystore boringssl engine + // in system/security/keystore-engine. + return KeyStore2.makeKeystoreEngineGrantString(key.nspace); + } + + private String getCaCertificateFromKeystoreAsPem(@NonNull KeyStore keystore, + @NonNull String alias) + throws KeyStoreException, IOException, CertificateEncodingException { + if (keystore.isCertificateEntry(alias)) { + final Certificate cert = keystore.getCertificate(alias); + if (cert == null) return null; + return new String(Credentials.convertToPem(cert), StandardCharsets.UTF_8); + } else { + final Certificate[] certs = keystore.getCertificateChain(alias); + // If there is none or one entry it means there is no CA entry associated with this + // alias. + if (certs == null || certs.length <= 1) { + return null; + } + // If this is not a (pure) certificate entry, then there is a user certificate which + // will be included at the beginning of the certificate chain. But the caller of this + // function does not expect this certificate to be included, so we cut it off. + return new String(Credentials.convertToPem( + Arrays.copyOfRange(certs, 1, certs.length)), StandardCharsets.UTF_8); + } + } + /** - * Like {@link #startLegacyVpn(VpnProfile, KeyStore, Network, LinkProperties)}, but does not + * Like {@link #startLegacyVpn(VpnProfile, Network, LinkProperties)}, but does not * check permissions under the assumption that the caller is the system. * * Callers are responsible for checking permissions if needed. */ - public void startLegacyVpnPrivileged(VpnProfile profile, KeyStore keyStore, + public void startLegacyVpnPrivileged(VpnProfile profile, @Nullable Network underlying, @NonNull LinkProperties egress) { UserInfo user = mUserManager.getUserInfo(mUserId); if (user.isRestricted() || mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN, @@ -2050,18 +2119,27 @@ public class Vpn { String userCert = ""; String caCert = ""; String serverCert = ""; - if (!profile.ipsecUserCert.isEmpty()) { - privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert; - byte[] value = keyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); - userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); - } - if (!profile.ipsecCaCert.isEmpty()) { - byte[] value = keyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); - caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); - } - if (!profile.ipsecServerCert.isEmpty()) { - byte[] value = keyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert); - serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); + + try { + final KeyStore keystore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER); + keystore.load(null); + if (!profile.ipsecUserCert.isEmpty()) { + privateKey = profile.ipsecUserCert; + final Certificate cert = keystore.getCertificate(profile.ipsecUserCert); + userCert = (cert == null) ? null + : new String(Credentials.convertToPem(cert), StandardCharsets.UTF_8); + } + if (!profile.ipsecCaCert.isEmpty()) { + caCert = getCaCertificateFromKeystoreAsPem(keystore, profile.ipsecCaCert); + } + if (!profile.ipsecServerCert.isEmpty()) { + final Certificate cert = keystore.getCertificate(profile.ipsecServerCert); + serverCert = (cert == null) ? null + : new String(Credentials.convertToPem(cert), StandardCharsets.UTF_8); + } + } catch (CertificateException | KeyStoreException | IOException + | NoSuchAlgorithmException e) { + throw new IllegalStateException("Failed to load credentials from AndroidKeyStore", e); } if (userCert == null || caCert == null || serverCert == null) { throw new IllegalStateException("Cannot load credentials"); @@ -2082,7 +2160,7 @@ public class Vpn { // Start VPN profile profile.setAllowedAlgorithms(Ikev2VpnProfile.DEFAULT_ALGORITHMS); - startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN, keyStore); + startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN); return; case VpnProfile.TYPE_IKEV2_IPSEC_PSK: // Ikev2VpnProfiles expect a base64-encoded preshared key. @@ -2091,7 +2169,7 @@ public class Vpn { // Start VPN profile profile.setAllowedAlgorithms(Ikev2VpnProfile.DEFAULT_ALGORITHMS); - startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN, keyStore); + startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN); return; case VpnProfile.TYPE_L2TP_IPSEC_PSK: racoon = new String[] { @@ -2101,8 +2179,8 @@ public class Vpn { break; case VpnProfile.TYPE_L2TP_IPSEC_RSA: racoon = new String[] { - iface, profile.server, "udprsa", privateKey, userCert, - caCert, serverCert, "1701", + iface, profile.server, "udprsa", makeKeystoreEngineGrantString(privateKey), + userCert, caCert, serverCert, "1701", }; break; case VpnProfile.TYPE_IPSEC_XAUTH_PSK: @@ -2113,8 +2191,8 @@ public class Vpn { break; case VpnProfile.TYPE_IPSEC_XAUTH_RSA: racoon = new String[] { - iface, profile.server, "xauthrsa", privateKey, userCert, - caCert, serverCert, profile.username, profile.password, "", gateway, + iface, profile.server, "xauthrsa", makeKeystoreEngineGrantString(privateKey), + userCert, caCert, serverCert, profile.username, profile.password, "", gateway, }; break; case VpnProfile.TYPE_IPSEC_HYBRID_RSA: @@ -3049,14 +3127,12 @@ public class Vpn { * * @param packageName the package name of the app provisioning this profile * @param profile the profile to be stored and provisioned - * @param keyStore the System keystore instance to save VPN profiles * @returns whether or not the app has already been granted user consent */ public synchronized boolean provisionVpnProfile( - @NonNull String packageName, @NonNull VpnProfile profile, @NonNull KeyStore keyStore) { + @NonNull String packageName, @NonNull VpnProfile profile) { checkNotNull(packageName, "No package name provided"); checkNotNull(profile, "No profile provided"); - checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); enforceNotRestrictedUser(); @@ -3075,11 +3151,9 @@ public class Vpn { // Permissions checked during startVpnProfile() Binder.withCleanCallingIdentity( () -> { - keyStore.put( + getVpnProfileStore().put( getProfileNameForPackage(packageName), - encodedProfile, - Process.SYSTEM_UID, - 0 /* flags */); + encodedProfile); }); // TODO: if package has CONTROL_VPN, grant the ACTIVATE_PLATFORM_VPN appop. @@ -3097,12 +3171,10 @@ public class Vpn { * Deletes an app-provisioned VPN profile. * * @param packageName the package name of the app provisioning this profile - * @param keyStore the System keystore instance to save VPN profiles */ public synchronized void deleteVpnProfile( - @NonNull String packageName, @NonNull KeyStore keyStore) { + @NonNull String packageName) { checkNotNull(packageName, "No package name provided"); - checkNotNull(keyStore, "KeyStore missing"); verifyCallingUidAndPackage(packageName); enforceNotRestrictedUser(); @@ -3114,13 +3186,13 @@ public class Vpn { if (isCurrentIkev2VpnLocked(packageName)) { if (mAlwaysOn) { // Will transitively call prepareInternal(VpnConfig.LEGACY_VPN). - setAlwaysOnPackage(null, false, null, keyStore); + setAlwaysOnPackage(null, false, null); } else { prepareInternal(VpnConfig.LEGACY_VPN); } } - keyStore.delete(getProfileNameForPackage(packageName), Process.SYSTEM_UID); + getVpnProfileStore().remove(getProfileNameForPackage(packageName)); }); } @@ -3132,13 +3204,13 @@ public class Vpn { */ @VisibleForTesting @Nullable - VpnProfile getVpnProfilePrivileged(@NonNull String packageName, @NonNull KeyStore keyStore) { + VpnProfile getVpnProfilePrivileged(@NonNull String packageName) { if (!mDeps.isCallerSystem()) { Log.wtf(TAG, "getVpnProfilePrivileged called as non-System UID "); return null; } - final byte[] encoded = keyStore.get(getProfileNameForPackage(packageName)); + final byte[] encoded = getVpnProfileStore().get(getProfileNameForPackage(packageName)); if (encoded == null) return null; return VpnProfile.decode("" /* Key unused */, encoded); @@ -3152,12 +3224,10 @@ public class Vpn { * will not match during appop checks. * * @param packageName the package name of the app provisioning this profile - * @param keyStore the System keystore instance to retrieve VPN profiles */ public synchronized void startVpnProfile( - @NonNull String packageName, @NonNull KeyStore keyStore) { + @NonNull String packageName) { checkNotNull(packageName, "No package name provided"); - checkNotNull(keyStore, "KeyStore missing"); enforceNotRestrictedUser(); @@ -3168,18 +3238,17 @@ public class Vpn { Binder.withCleanCallingIdentity( () -> { - final VpnProfile profile = getVpnProfilePrivileged(packageName, keyStore); + final VpnProfile profile = getVpnProfilePrivileged(packageName); if (profile == null) { throw new IllegalArgumentException("No profile found for " + packageName); } - startVpnProfilePrivileged(profile, packageName, - null /* keyStore for private key retrieval - unneeded */); + startVpnProfilePrivileged(profile, packageName); }); } private synchronized void startVpnProfilePrivileged( - @NonNull VpnProfile profile, @NonNull String packageName, @Nullable KeyStore keyStore) { + @NonNull VpnProfile profile, @NonNull String packageName) { // Make sure VPN is prepared. This method can be called by user apps via startVpnProfile(), // by the Setting app via startLegacyVpn(), or by ConnectivityService via // startAlwaysOnVpn(), so this is the common place to prepare the VPN. This also has the @@ -3210,7 +3279,7 @@ public class Vpn { case VpnProfile.TYPE_IKEV2_IPSEC_PSK: case VpnProfile.TYPE_IKEV2_IPSEC_RSA: mVpnRunner = - new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile, keyStore)); + new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile)); mVpnRunner.start(); break; default: @@ -3218,7 +3287,7 @@ public class Vpn { Log.d(TAG, "Unknown VPN profile type: " + profile.type); break; } - } catch (IOException | GeneralSecurityException e) { + } catch (GeneralSecurityException e) { // Reset mConfig mConfig = null; diff --git a/services/core/java/com/android/server/connectivity/VpnProfileStore.java b/services/core/java/com/android/server/connectivity/VpnProfileStore.java new file mode 100644 index 000000000000..2f8aebf07e49 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/VpnProfileStore.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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.connectivity; + +import android.annotation.NonNull; +import android.security.LegacyVpnProfileStore; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Mockable indirection to the actual profile store. + * @hide + */ +public class VpnProfileStore { + /** + * Stores the profile under the alias in the profile database. Existing profiles by the + * same name will be replaced. + * @param alias The name of the profile + * @param profile The profile. + * @return true if the profile was successfully added. False otherwise. + * @hide + */ + @VisibleForTesting + public boolean put(@NonNull String alias, @NonNull byte[] profile) { + return LegacyVpnProfileStore.put(alias, profile); + } + + /** + * Retrieves a profile by the name alias from the profile database. + * @param alias Name of the profile to retrieve. + * @return The unstructured blob, that is the profile that was stored using + * LegacyVpnProfileStore#put or with + * android.security.Keystore.put(Credentials.VPN + alias). + * Returns null if no profile was found. + * @hide + */ + @VisibleForTesting + public byte[] get(@NonNull String alias) { + return LegacyVpnProfileStore.get(alias); + } + + /** + * Removes a profile by the name alias from the profile database. + * @param alias Name of the profile to be removed. + * @return True if a profile was removed. False if no such profile was found. + * @hide + */ + @VisibleForTesting + public boolean remove(@NonNull String alias) { + return LegacyVpnProfileStore.remove(alias); + } + + /** + * Lists the vpn profiles stored in the database. + * @return An array of strings representing the aliases stored in the profile database. + * The return value may be empty but never null. + * @hide + */ + @VisibleForTesting + public @NonNull String[] list(@NonNull String prefix) { + return LegacyVpnProfileStore.list(prefix); + } +} diff --git a/services/core/java/com/android/server/net/LockdownVpnTracker.java b/services/core/java/com/android/server/net/LockdownVpnTracker.java index 3cc32bef0e67..851ea3d01085 100644 --- a/services/core/java/com/android/server/net/LockdownVpnTracker.java +++ b/services/core/java/com/android/server/net/LockdownVpnTracker.java @@ -35,7 +35,6 @@ import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.os.Handler; -import android.security.KeyStore; import android.text.TextUtils; import android.util.Log; @@ -63,7 +62,6 @@ public class LockdownVpnTracker { @NonNull private final Handler mHandler; @NonNull private final Vpn mVpn; @NonNull private final VpnProfile mProfile; - @NonNull private final KeyStore mKeyStore; @NonNull private final Object mStateLock = new Object(); @@ -132,7 +130,6 @@ public class LockdownVpnTracker { public LockdownVpnTracker(@NonNull Context context, @NonNull Handler handler, - @NonNull KeyStore keyStore, @NonNull Vpn vpn, @NonNull VpnProfile profile) { mContext = Objects.requireNonNull(context); @@ -140,7 +137,6 @@ public class LockdownVpnTracker { mHandler = Objects.requireNonNull(handler); mVpn = Objects.requireNonNull(vpn); mProfile = Objects.requireNonNull(profile); - mKeyStore = Objects.requireNonNull(keyStore); mNotificationManager = mContext.getSystemService(NotificationManager.class); final Intent configIntent = new Intent(ACTION_VPN_SETTINGS); @@ -212,7 +208,7 @@ public class LockdownVpnTracker { // network is the system default. So, if the VPN is up and underlying network // (e.g., wifi) disconnects, CS will inform apps that the VPN's capabilities have // changed to match the new default network (e.g., cell). - mVpn.startLegacyVpnPrivileged(mProfile, mKeyStore, network, egressProp); + mVpn.startLegacyVpnPrivileged(mProfile, network, egressProp); } catch (IllegalStateException e) { mAcceptedEgressIface = null; Log.e(TAG, "Failed to start VPN", e); diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 10ec981e42a2..8fef9f8c7180 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -244,7 +244,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.security.Credentials; -import android.security.KeyStore; import android.system.Os; import android.telephony.TelephonyManager; import android.telephony.data.EpsBearerQosSessionAttributes; @@ -276,6 +275,7 @@ import com.android.server.connectivity.NetworkNotificationManager.NotificationTy import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.VpnProfileStore; import com.android.server.net.NetworkPinner; import com.android.server.net.NetworkPolicyManagerInternal; import com.android.testutils.ExceptionUtils; @@ -431,7 +431,7 @@ public class ConnectivityServiceTest { @Mock MockableSystemProperties mSystemProperties; @Mock EthernetManager mEthernetManager; @Mock NetworkPolicyManager mNetworkPolicyManager; - @Mock KeyStore mKeyStore; + @Mock VpnProfileStore mVpnProfileStore; @Mock SystemConfigManager mSystemConfigManager; private ArgumentCaptor mResolverParamsParcelCaptor = @@ -1115,7 +1115,7 @@ public class ConnectivityServiceTest { return mDeviceIdleInternal; } }, - mNetworkManagementService, mMockNetd, userId, mKeyStore); + mNetworkManagementService, mMockNetd, userId, mVpnProfileStore); } public void setUids(Set uids) { @@ -1294,8 +1294,9 @@ public class ConnectivityServiceTest { return mVMSHandlerThread; } - public KeyStore getKeyStore() { - return mKeyStore; + @Override + public VpnProfileStore getVpnProfileStore() { + return mVpnProfileStore; } public INetd getNetd() { @@ -7487,8 +7488,7 @@ public class ConnectivityServiceTest { private void setupLegacyLockdownVpn() { final String profileName = "testVpnProfile"; final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8); - when(mKeyStore.contains(Credentials.LOCKDOWN_VPN)).thenReturn(true); - when(mKeyStore.get(Credentials.LOCKDOWN_VPN)).thenReturn(profileTag); + when(mVpnProfileStore.get(Credentials.LOCKDOWN_VPN)).thenReturn(profileTag); final VpnProfile profile = new VpnProfile(profileName); profile.name = "My VPN"; @@ -7496,7 +7496,7 @@ public class ConnectivityServiceTest { profile.dnsServers = "8.8.8.8"; profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK; final byte[] encodedProfile = profile.encode(); - when(mKeyStore.get(Credentials.VPN + profileName)).thenReturn(encodedProfile); + when(mVpnProfileStore.get(Credentials.VPN + profileName)).thenReturn(encodedProfile); } private void establishLegacyLockdownVpn(Network underlying) throws Exception { diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 7489a0f889dc..b8f7fbca3983 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -91,7 +91,6 @@ import android.os.UserManager; import android.os.test.TestLooper; import android.provider.Settings; import android.security.Credentials; -import android.security.KeyStore; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Range; @@ -196,7 +195,7 @@ public class VpnTest { @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; @Mock private ConnectivityManager mConnectivityManager; @Mock private IpSecService mIpSecService; - @Mock private KeyStore mKeyStore; + @Mock private VpnProfileStore mVpnProfileStore; private final VpnProfile mVpnProfile; private IpSecManager mIpSecManager; @@ -333,17 +332,17 @@ public class VpnTest { assertFalse(vpn.getLockdown()); // Set always-on without lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList(), mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); assertTrue(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); // Set always-on with lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList(), mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); assertTrue(vpn.getAlwaysOn()); assertTrue(vpn.getLockdown()); // Remove always-on configuration. - assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList(), mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); assertFalse(vpn.getAlwaysOn()); assertFalse(vpn.getLockdown()); } @@ -354,17 +353,17 @@ public class VpnTest { final UidRange user = PRI_USER_RANGE; // Set always-on without lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null, mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); // Set always-on with lockdown. - assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null, mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1), new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop) })); // Switch to another app. - assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1), new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop) @@ -382,14 +381,14 @@ public class VpnTest { // Set always-on with lockdown and allow app PKGS[2] from lockdown. assertTrue(vpn.setAlwaysOnPackage( - PKGS[1], true, Collections.singletonList(PKGS[2]), mKeyStore)); + PKGS[1], true, Collections.singletonList(PKGS[2]))); verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1), new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop) })); // Change allowed app list to PKGS[3]. assertTrue(vpn.setAlwaysOnPackage( - PKGS[1], true, Collections.singletonList(PKGS[3]), mKeyStore)); + PKGS[1], true, Collections.singletonList(PKGS[3]))); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop) })); @@ -400,7 +399,7 @@ public class VpnTest { // Change the VPN app. assertTrue(vpn.setAlwaysOnPackage( - PKGS[0], true, Collections.singletonList(PKGS[3]), mKeyStore)); + PKGS[0], true, Collections.singletonList(PKGS[3]))); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1), new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1) @@ -411,7 +410,7 @@ public class VpnTest { })); // Remove the list of allowed packages. - assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null, mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1), new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop) @@ -422,7 +421,7 @@ public class VpnTest { // Add the list of allowed packages. assertTrue(vpn.setAlwaysOnPackage( - PKGS[0], true, Collections.singletonList(PKGS[1]), mKeyStore)); + PKGS[0], true, Collections.singletonList(PKGS[1]))); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.stop) })); @@ -433,12 +432,12 @@ public class VpnTest { // Try allowing a package with a comma, should be rejected. assertFalse(vpn.setAlwaysOnPackage( - PKGS[0], true, Collections.singletonList("a.b,c.d"), mKeyStore)); + PKGS[0], true, Collections.singletonList("a.b,c.d"))); // Pass a non-existent packages in the allowlist, they (and only they) should be ignored. // allowed package should change from PGKS[1] to PKGS[2]. assertTrue(vpn.setAlwaysOnPackage( - PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"), mKeyStore)); + PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"))); verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1), new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop) @@ -525,22 +524,22 @@ public class VpnTest { .thenReturn(Collections.singletonList(resInfo)); // null package name should return false - assertFalse(vpn.isAlwaysOnPackageSupported(null, mKeyStore)); + assertFalse(vpn.isAlwaysOnPackageSupported(null)); // Pre-N apps are not supported appInfo.targetSdkVersion = VERSION_CODES.M; - assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); // N+ apps are supported by default appInfo.targetSdkVersion = VERSION_CODES.N; - assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); + assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0])); // Apps that opt out explicitly are not supported appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; Bundle metaData = new Bundle(); metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false); svcInfo.metaData = metaData; - assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0], mKeyStore)); + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); } @Test @@ -556,7 +555,7 @@ public class VpnTest { order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt()); // Start showing a notification for disconnected once always-on. - vpn.setAlwaysOnPackage(PKGS[0], false, null, mKeyStore); + vpn.setAlwaysOnPackage(PKGS[0], false, null); order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); // Stop showing the notification once connected. @@ -568,7 +567,7 @@ public class VpnTest { order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); // Notification should be cleared after unsetting always-on package. - vpn.setAlwaysOnPackage(null, false, null, mKeyStore); + vpn.setAlwaysOnPackage(null, false, null); order.verify(mNotificationManager).cancel(anyString(), anyInt()); } @@ -608,15 +607,13 @@ public class VpnTest { } private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) { - assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore)); + assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile)); // The profile should always be stored, whether or not consent has been previously granted. - verify(mKeyStore) + verify(mVpnProfileStore) .put( eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), - eq(mVpnProfile.encode()), - eq(Process.SYSTEM_UID), - eq(0)); + eq(mVpnProfile.encode())); for (final String checkedOpStr : checkedOps) { verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG, @@ -671,7 +668,7 @@ public class VpnTest { bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]); try { - vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile, mKeyStore); + vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile); fail("Expected IAE due to profile size"); } catch (IllegalArgumentException expected) { } @@ -684,7 +681,7 @@ public class VpnTest { restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); try { - vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore); + vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile); fail("Expected SecurityException due to restricted user"); } catch (SecurityException expected) { } @@ -694,10 +691,10 @@ public class VpnTest { public void testDeleteVpnProfile() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); - vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.deleteVpnProfile(TEST_VPN_PKG); - verify(mKeyStore) - .delete(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), eq(Process.SYSTEM_UID)); + verify(mVpnProfileStore) + .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); } @Test @@ -707,7 +704,7 @@ public class VpnTest { restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); try { - vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.deleteVpnProfile(TEST_VPN_PKG); fail("Expected SecurityException due to restricted user"); } catch (SecurityException expected) { } @@ -717,24 +714,24 @@ public class VpnTest { public void testGetVpnProfilePrivileged() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(); - when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) .thenReturn(new VpnProfile("").encode()); - vpn.getVpnProfilePrivileged(TEST_VPN_PKG, mKeyStore); + vpn.getVpnProfilePrivileged(TEST_VPN_PKG); - verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); } @Test public void testStartVpnProfile() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); - when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) .thenReturn(mVpnProfile.encode()); - vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.startVpnProfile(TEST_VPN_PKG); - verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); verify(mAppOps) .noteOpNoThrow( eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), @@ -748,10 +745,10 @@ public class VpnTest { public void testStartVpnProfileVpnServicePreconsented() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN); - when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) .thenReturn(mVpnProfile.encode()); - vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.startVpnProfile(TEST_VPN_PKG); // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown. verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), @@ -763,7 +760,7 @@ public class VpnTest { final Vpn vpn = createVpnAndSetupUidChecks(); try { - vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.startVpnProfile(TEST_VPN_PKG); fail("Expected failure due to no user consent"); } catch (SecurityException expected) { } @@ -780,22 +777,22 @@ public class VpnTest { TEST_VPN_PKG, null /* attributionTag */, null /* message */); // Keystore should never have been accessed. - verify(mKeyStore, never()).get(any()); + verify(mVpnProfileStore, never()).get(any()); } @Test public void testStartVpnProfileMissingProfile() throws Exception { final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); - when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); try { - vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.startVpnProfile(TEST_VPN_PKG); fail("Expected failure due to missing profile"); } catch (IllegalArgumentException expected) { } - verify(mKeyStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); + verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); verify(mAppOps) .noteOpNoThrow( eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), @@ -812,7 +809,7 @@ public class VpnTest { restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); try { - vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore); + vpn.startVpnProfile(TEST_VPN_PKG); fail("Expected SecurityException due to restricted user"); } catch (SecurityException expected) { } @@ -938,9 +935,9 @@ public class VpnTest { } private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) { - assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null, mKeyStore)); + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null)); - verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); verify(mAppOps).setMode( eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG), eq(AppOpsManager.MODE_ALLOWED)); @@ -963,11 +960,11 @@ public class VpnTest { final int uid = Process.myUid() + 1; when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) .thenReturn(uid); - when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) .thenReturn(mVpnProfile.encode()); setAndVerifyAlwaysOnPackage(vpn, uid, false); - assertTrue(vpn.startAlwaysOnVpn(mKeyStore)); + assertTrue(vpn.startAlwaysOnVpn()); // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in // a subsequent CL. @@ -984,7 +981,7 @@ public class VpnTest { InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE); lp.addRoute(defaultRoute); - vpn.startLegacyVpn(vpnProfile, mKeyStore, EGRESS_NETWORK, lp); + vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp); return vpn; } @@ -1186,7 +1183,7 @@ public class VpnTest { .thenReturn(asUserContext); final TestLooper testLooper = new TestLooper(); final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService, - mNetd, userId, mKeyStore, mSystemServices, mIkev2SessionCreator); + mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator); verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat( provider -> provider.getName().contains("VpnNetworkProvider") ));