Keystore 2.0: Make VPN Keystore 2.0 ready.

* Use public stable API to load certificates from keystore.
* Also use grants to allow racoon to use keystore keys without
  special exceptions in keystore.
* Use LegacyProfileStore instead of Keystore for storing VPN profiles.

Bug: 175068876
Bug: 171305607
Test: atest android.net.cts.Ikev2VpnTest
      atest android.net.cts.IpSecManagerTest
      atest com.android.server.connectivity.VpnTest
      atest com.android.server.ConnectivityServiceTest
Change-Id: I27975113896ea137260a9f94a34fb1c3ca173fe3
This commit is contained in:
Janis Danisevskis 2021-01-19 17:49:26 -08:00
parent d3c8a4011c
commit 4ba2f0c690
9 changed files with 462 additions and 183 deletions

View File

@ -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",

View File

@ -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.
*
* <p>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 {

View File

@ -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];
}
}

View File

@ -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 {

View File

@ -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 {
* </ul>
*
* @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<String> lockdownAllowlist,
@NonNull KeyStore keyStore) {
@Nullable List<String> 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<String> lockdownAllowlist, @NonNull KeyStore keyStore) {
@Nullable List<String> 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<String> 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;

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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<ResolverParamsParcel> mResolverParamsParcelCaptor =
@ -1115,7 +1115,7 @@ public class ConnectivityServiceTest {
return mDeviceIdleInternal;
}
},
mNetworkManagementService, mMockNetd, userId, mKeyStore);
mNetworkManagementService, mMockNetd, userId, mVpnProfileStore);
}
public void setUids(Set<UidRange> 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 {

View File

@ -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")
));