diff options
Diffstat (limited to 'services/core/java/com/android/server/connectivity/Vpn.java')
-rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 215 |
1 files changed, 142 insertions, 73 deletions
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 01ac81fb2cb5..124c3741ad57 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -100,7 +100,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; @@ -131,6 +136,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; @@ -156,6 +167,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. @@ -215,6 +227,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. @@ -392,24 +411,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 */); @@ -445,7 +465,7 @@ public class Vpn { mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE)); - loadAlwaysOnPackage(keyStore); + loadAlwaysOnPackage(); } /** @@ -566,11 +586,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) { @@ -579,7 +597,7 @@ public class Vpn { final long oldId = Binder.clearCallingIdentity(); try { - if (getVpnProfilePrivileged(packageName, keyStore) != null) { + if (getVpnProfilePrivileged(packageName) != null) { return true; } } finally { @@ -631,17 +649,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; } @@ -658,13 +674,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; @@ -683,7 +698,7 @@ public class Vpn { final VpnProfile profile; final long oldId = Binder.clearCallingIdentity(); try { - profile = getVpnProfilePrivileged(packageName, keyStore); + profile = getVpnProfilePrivileged(packageName); } finally { Binder.restoreCallingIdentity(oldId); } @@ -758,7 +773,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( @@ -770,7 +785,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); } @@ -779,11 +794,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(); @@ -792,8 +806,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 @@ -807,10 +821,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 @@ -2011,27 +2024,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, @@ -2048,18 +2117,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"); @@ -2080,7 +2158,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. @@ -2089,7 +2167,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[] { @@ -2099,8 +2177,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: @@ -2111,8 +2189,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: @@ -3047,14 +3125,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(); @@ -3073,11 +3149,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. @@ -3095,12 +3169,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(); @@ -3112,13 +3184,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)); }); } @@ -3130,13 +3202,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); @@ -3150,12 +3222,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(); @@ -3166,18 +3236,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 @@ -3208,7 +3277,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: @@ -3216,7 +3285,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; |