/* * 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 android.net.vcn.persistablebundleutils; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; import static com.android.internal.annotations.VisibleForTesting.Visibility; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.InetAddresses; import android.net.eap.EapSessionConfig; import android.net.ipsec.ike.IkeSaProposal; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv4PcscfServer; import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv6PcscfServer; import android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig; import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignLocalConfig; import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignRemoteConfig; import android.net.ipsec.ike.IkeSessionParams.IkeAuthEapConfig; import android.net.ipsec.ike.IkeSessionParams.IkeAuthPskConfig; import android.net.ipsec.ike.IkeSessionParams.IkeConfigRequest; import android.os.PersistableBundle; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.vcn.util.PersistableBundleUtils; import java.net.InetAddress; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; /** * Abstract utility class to convert IkeSessionParams to/from PersistableBundle. * * @hide */ @VisibleForTesting(visibility = Visibility.PRIVATE) public final class IkeSessionParamsUtils { private static final String TAG = IkeSessionParamsUtils.class.getSimpleName(); private static final String SERVER_HOST_NAME_KEY = "SERVER_HOST_NAME_KEY"; private static final String SA_PROPOSALS_KEY = "SA_PROPOSALS_KEY"; private static final String LOCAL_ID_KEY = "LOCAL_ID_KEY"; private static final String REMOTE_ID_KEY = "REMOTE_ID_KEY"; private static final String LOCAL_AUTH_KEY = "LOCAL_AUTH_KEY"; private static final String REMOTE_AUTH_KEY = "REMOTE_AUTH_KEY"; private static final String CONFIG_REQUESTS_KEY = "CONFIG_REQUESTS_KEY"; private static final String RETRANS_TIMEOUTS_KEY = "RETRANS_TIMEOUTS_KEY"; private static final String HARD_LIFETIME_SEC_KEY = "HARD_LIFETIME_SEC_KEY"; private static final String SOFT_LIFETIME_SEC_KEY = "SOFT_LIFETIME_SEC_KEY"; private static final String DPD_DELAY_SEC_KEY = "DPD_DELAY_SEC_KEY"; private static final String NATT_KEEPALIVE_DELAY_SEC_KEY = "NATT_KEEPALIVE_DELAY_SEC_KEY"; private static final String IKE_OPTIONS_KEY = "IKE_OPTIONS_KEY"; private static final String IP_VERSION_KEY = "IP_VERSION_KEY"; private static final String ENCAP_TYPE_KEY = "ENCAP_TYPE_KEY"; // TODO: add DSCP_KEY and IS_IKE_FRAGMENT_SUPPORTED_KEY. // TODO: b/243181760 Use the IKE API when they are exposed @VisibleForTesting(visibility = Visibility.PRIVATE) public static final int IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION = 6; @VisibleForTesting(visibility = Visibility.PRIVATE) public static final int IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES = 7; private static final Set IKE_OPTIONS = new ArraySet<>(); static { IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_MOBIKE); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY); IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION); IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_AUTOMATIC_KEEPALIVE_ON_OFF); } /** * Check if an IKE option is supported in the IPsec module installed on the device * *

This method ensures caller to safely access options that are added between dessert * releases. */ @VisibleForTesting(visibility = Visibility.PRIVATE) public static boolean isIkeOptionValid(int option) { try { new IkeSessionParams.Builder().addIkeOption(option); return true; } catch (IllegalArgumentException e) { Log.d(TAG, "Option not supported; discarding: " + option); return false; } } /** Serializes an IkeSessionParams to a PersistableBundle. */ @NonNull public static PersistableBundle toPersistableBundle(@NonNull IkeSessionParams params) { if (params.getNetwork() != null || params.getIke3gppExtension() != null) { throw new IllegalStateException( "Cannot convert a IkeSessionParams with a caller configured network or with" + " 3GPP extension enabled"); } final PersistableBundle result = new PersistableBundle(); result.putString(SERVER_HOST_NAME_KEY, params.getServerHostname()); final PersistableBundle saProposalBundle = PersistableBundleUtils.fromList( params.getSaProposals(), IkeSaProposalUtils::toPersistableBundle); result.putPersistableBundle(SA_PROPOSALS_KEY, saProposalBundle); result.putPersistableBundle( LOCAL_ID_KEY, IkeIdentificationUtils.toPersistableBundle(params.getLocalIdentification())); result.putPersistableBundle( REMOTE_ID_KEY, IkeIdentificationUtils.toPersistableBundle(params.getRemoteIdentification())); result.putPersistableBundle( LOCAL_AUTH_KEY, AuthConfigUtils.toPersistableBundle(params.getLocalAuthConfig())); result.putPersistableBundle( REMOTE_AUTH_KEY, AuthConfigUtils.toPersistableBundle(params.getRemoteAuthConfig())); final List reqList = new ArrayList<>(); for (IkeConfigRequest req : params.getConfigurationRequests()) { reqList.add(new ConfigRequest(req)); } final PersistableBundle configReqListBundle = PersistableBundleUtils.fromList(reqList, ConfigRequest::toPersistableBundle); result.putPersistableBundle(CONFIG_REQUESTS_KEY, configReqListBundle); result.putIntArray(RETRANS_TIMEOUTS_KEY, params.getRetransmissionTimeoutsMillis()); result.putInt(HARD_LIFETIME_SEC_KEY, params.getHardLifetimeSeconds()); result.putInt(SOFT_LIFETIME_SEC_KEY, params.getSoftLifetimeSeconds()); result.putInt(DPD_DELAY_SEC_KEY, params.getDpdDelaySeconds()); result.putInt(NATT_KEEPALIVE_DELAY_SEC_KEY, params.getNattKeepAliveDelaySeconds()); result.putInt(IP_VERSION_KEY, params.getIpVersion()); result.putInt(ENCAP_TYPE_KEY, params.getEncapType()); final List enabledIkeOptions = new ArrayList<>(); try { // TODO: b/328844044: Ideally this code should gate the behavior by checking the // com.android.ipsec.flags.enabled_ike_options_api flag but that flag is not accessible // right now. We should either update the code when the flag is accessible or remove the // legacy behavior after VIC SDK finalization enabledIkeOptions.addAll(params.getIkeOptions()); } catch (Exception e) { // getIkeOptions throws. It means the API is not available enabledIkeOptions.clear(); for (int option : IKE_OPTIONS) { if (isIkeOptionValid(option) && params.hasIkeOption(option)) { enabledIkeOptions.add(option); } } } final int[] optionArray = enabledIkeOptions.stream().mapToInt(i -> i).toArray(); result.putIntArray(IKE_OPTIONS_KEY, optionArray); return result; } /** Constructs an IkeSessionParams by deserializing a PersistableBundle. */ @NonNull public static IkeSessionParams fromPersistableBundle(@NonNull PersistableBundle in) { Objects.requireNonNull(in, "PersistableBundle is null"); final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(); builder.setServerHostname(in.getString(SERVER_HOST_NAME_KEY)); PersistableBundle proposalBundle = in.getPersistableBundle(SA_PROPOSALS_KEY); Objects.requireNonNull(in, "SA Proposals was null"); List saProposals = PersistableBundleUtils.toList( proposalBundle, IkeSaProposalUtils::fromPersistableBundle); for (IkeSaProposal proposal : saProposals) { builder.addSaProposal(proposal); } builder.setLocalIdentification( IkeIdentificationUtils.fromPersistableBundle( in.getPersistableBundle(LOCAL_ID_KEY))); builder.setRemoteIdentification( IkeIdentificationUtils.fromPersistableBundle( in.getPersistableBundle(REMOTE_ID_KEY))); AuthConfigUtils.setBuilderByReadingPersistableBundle( in.getPersistableBundle(LOCAL_AUTH_KEY), in.getPersistableBundle(REMOTE_AUTH_KEY), builder); builder.setRetransmissionTimeoutsMillis(in.getIntArray(RETRANS_TIMEOUTS_KEY)); builder.setLifetimeSeconds( in.getInt(HARD_LIFETIME_SEC_KEY), in.getInt(SOFT_LIFETIME_SEC_KEY)); builder.setDpdDelaySeconds(in.getInt(DPD_DELAY_SEC_KEY)); builder.setNattKeepAliveDelaySeconds(in.getInt(NATT_KEEPALIVE_DELAY_SEC_KEY)); builder.setIpVersion(in.getInt(IP_VERSION_KEY)); builder.setEncapType(in.getInt(ENCAP_TYPE_KEY)); final PersistableBundle configReqListBundle = in.getPersistableBundle(CONFIG_REQUESTS_KEY); Objects.requireNonNull(configReqListBundle, "Config request list was null"); final List reqList = PersistableBundleUtils.toList(configReqListBundle, ConfigRequest::new); for (ConfigRequest req : reqList) { switch (req.type) { case ConfigRequest.IPV4_P_CSCF_ADDRESS: if (req.address == null) { builder.addPcscfServerRequest(AF_INET); } else { builder.addPcscfServerRequest(req.address); } break; case ConfigRequest.IPV6_P_CSCF_ADDRESS: if (req.address == null) { builder.addPcscfServerRequest(AF_INET6); } else { builder.addPcscfServerRequest(req.address); } break; default: throw new IllegalArgumentException( "Unrecognized config request type: " + req.type); } } // Clear IKE Options that are by default enabled for (int option : IKE_OPTIONS) { if (isIkeOptionValid(option)) { builder.removeIkeOption(option); } } final int[] optionArray = in.getIntArray(IKE_OPTIONS_KEY); for (int option : optionArray) { if (isIkeOptionValid(option)) { builder.addIkeOption(option); } } return builder.build(); } private static final class AuthConfigUtils { private static final int IKE_AUTH_METHOD_PSK = 1; private static final int IKE_AUTH_METHOD_PUB_KEY_SIGNATURE = 2; private static final int IKE_AUTH_METHOD_EAP = 3; private static final String AUTH_METHOD_KEY = "AUTH_METHOD_KEY"; @NonNull public static PersistableBundle toPersistableBundle(@NonNull IkeAuthConfig authConfig) { if (authConfig instanceof IkeAuthPskConfig) { IkeAuthPskConfig config = (IkeAuthPskConfig) authConfig; return IkeAuthPskConfigUtils.toPersistableBundle( config, createPersistableBundle(IKE_AUTH_METHOD_PSK)); } else if (authConfig instanceof IkeAuthDigitalSignLocalConfig) { IkeAuthDigitalSignLocalConfig config = (IkeAuthDigitalSignLocalConfig) authConfig; return IkeAuthDigitalSignConfigUtils.toPersistableBundle( config, createPersistableBundle(IKE_AUTH_METHOD_PUB_KEY_SIGNATURE)); } else if (authConfig instanceof IkeAuthDigitalSignRemoteConfig) { IkeAuthDigitalSignRemoteConfig config = (IkeAuthDigitalSignRemoteConfig) authConfig; return IkeAuthDigitalSignConfigUtils.toPersistableBundle( config, createPersistableBundle(IKE_AUTH_METHOD_PUB_KEY_SIGNATURE)); } else if (authConfig instanceof IkeAuthEapConfig) { IkeAuthEapConfig config = (IkeAuthEapConfig) authConfig; return IkeAuthEapConfigUtils.toPersistableBundle( config, createPersistableBundle(IKE_AUTH_METHOD_EAP)); } else { throw new IllegalStateException("Invalid IkeAuthConfig subclass"); } } private static PersistableBundle createPersistableBundle(int type) { final PersistableBundle result = new PersistableBundle(); result.putInt(AUTH_METHOD_KEY, type); return result; } public static void setBuilderByReadingPersistableBundle( @NonNull PersistableBundle localAuthBundle, @NonNull PersistableBundle remoteAuthBundle, @NonNull IkeSessionParams.Builder builder) { Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); final int localMethodType = localAuthBundle.getInt(AUTH_METHOD_KEY); final int remoteMethodType = remoteAuthBundle.getInt(AUTH_METHOD_KEY); switch (localMethodType) { case IKE_AUTH_METHOD_PSK: if (remoteMethodType != IKE_AUTH_METHOD_PSK) { throw new IllegalArgumentException( "Expect remote auth method to be PSK based, but was " + remoteMethodType); } IkeAuthPskConfigUtils.setBuilderByReadingPersistableBundle( localAuthBundle, remoteAuthBundle, builder); return; case IKE_AUTH_METHOD_PUB_KEY_SIGNATURE: if (remoteMethodType != IKE_AUTH_METHOD_PUB_KEY_SIGNATURE) { throw new IllegalArgumentException( "Expect remote auth method to be digital signature based, but was " + remoteMethodType); } IkeAuthDigitalSignConfigUtils.setBuilderByReadingPersistableBundle( localAuthBundle, remoteAuthBundle, builder); return; case IKE_AUTH_METHOD_EAP: if (remoteMethodType != IKE_AUTH_METHOD_PUB_KEY_SIGNATURE) { throw new IllegalArgumentException( "When using EAP for local authentication, expect remote auth" + " method to be digital signature based, but was " + remoteMethodType); } IkeAuthEapConfigUtils.setBuilderByReadingPersistableBundle( localAuthBundle, remoteAuthBundle, builder); return; default: throw new IllegalArgumentException( "Invalid EAP method type " + localMethodType); } } } private static final class IkeAuthPskConfigUtils { private static final String PSK_KEY = "PSK_KEY"; @NonNull public static PersistableBundle toPersistableBundle( @NonNull IkeAuthPskConfig config, @NonNull PersistableBundle result) { result.putPersistableBundle( PSK_KEY, PersistableBundleUtils.fromByteArray(config.getPsk())); return result; } public static void setBuilderByReadingPersistableBundle( @NonNull PersistableBundle localAuthBundle, @NonNull PersistableBundle remoteAuthBundle, @NonNull IkeSessionParams.Builder builder) { Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); final PersistableBundle localPskBundle = localAuthBundle.getPersistableBundle(PSK_KEY); final PersistableBundle remotePskBundle = remoteAuthBundle.getPersistableBundle(PSK_KEY); Objects.requireNonNull(localAuthBundle, "Local PSK was null"); Objects.requireNonNull(remoteAuthBundle, "Remote PSK was null"); final byte[] localPsk = PersistableBundleUtils.toByteArray(localPskBundle); final byte[] remotePsk = PersistableBundleUtils.toByteArray(remotePskBundle); if (!Arrays.equals(localPsk, remotePsk)) { throw new IllegalArgumentException("Local PSK and remote PSK are different"); } builder.setAuthPsk(localPsk); } } private static class IkeAuthDigitalSignConfigUtils { private static final String END_CERT_KEY = "END_CERT_KEY"; private static final String INTERMEDIATE_CERTS_KEY = "INTERMEDIATE_CERTS_KEY"; private static final String PRIVATE_KEY_KEY = "PRIVATE_KEY_KEY"; private static final String TRUST_CERT_KEY = "TRUST_CERT_KEY"; @NonNull public static PersistableBundle toPersistableBundle( @NonNull IkeAuthDigitalSignLocalConfig config, @NonNull PersistableBundle result) { try { result.putPersistableBundle( END_CERT_KEY, PersistableBundleUtils.fromByteArray( config.getClientEndCertificate().getEncoded())); final List certList = config.getIntermediateCertificates(); final List encodedCertList = new ArrayList<>(certList.size()); for (X509Certificate cert : certList) { encodedCertList.add(cert.getEncoded()); } final PersistableBundle certsBundle = PersistableBundleUtils.fromList( encodedCertList, PersistableBundleUtils::fromByteArray); result.putPersistableBundle(INTERMEDIATE_CERTS_KEY, certsBundle); } catch (CertificateEncodingException e) { throw new IllegalArgumentException("Fail to encode certificate"); } // TODO: b/170670506 Consider putting PrivateKey in Android KeyStore result.putPersistableBundle( PRIVATE_KEY_KEY, PersistableBundleUtils.fromByteArray(config.getPrivateKey().getEncoded())); return result; } @NonNull public static PersistableBundle toPersistableBundle( @NonNull IkeAuthDigitalSignRemoteConfig config, @NonNull PersistableBundle result) { try { X509Certificate caCert = config.getRemoteCaCert(); if (caCert != null) { result.putPersistableBundle( TRUST_CERT_KEY, PersistableBundleUtils.fromByteArray(caCert.getEncoded())); } } catch (CertificateEncodingException e) { throw new IllegalArgumentException("Fail to encode the certificate"); } return result; } public static void setBuilderByReadingPersistableBundle( @NonNull PersistableBundle localAuthBundle, @NonNull PersistableBundle remoteAuthBundle, @NonNull IkeSessionParams.Builder builder) { Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); // Deserialize localAuth final PersistableBundle endCertBundle = localAuthBundle.getPersistableBundle(END_CERT_KEY); Objects.requireNonNull(endCertBundle, "End cert was null"); final byte[] encodedCert = PersistableBundleUtils.toByteArray(endCertBundle); final X509Certificate endCert = CertUtils.certificateFromByteArray(encodedCert); final PersistableBundle certsBundle = localAuthBundle.getPersistableBundle(INTERMEDIATE_CERTS_KEY); Objects.requireNonNull(certsBundle, "Intermediate certs was null"); final List encodedCertList = PersistableBundleUtils.toList(certsBundle, PersistableBundleUtils::toByteArray); final List certList = new ArrayList<>(encodedCertList.size()); for (byte[] encoded : encodedCertList) { certList.add(CertUtils.certificateFromByteArray(encoded)); } final PersistableBundle privateKeyBundle = localAuthBundle.getPersistableBundle(PRIVATE_KEY_KEY); Objects.requireNonNull(privateKeyBundle, "PrivateKey bundle was null"); final PrivateKey privateKey = CertUtils.privateKeyFromByteArray( PersistableBundleUtils.toByteArray(privateKeyBundle)); // Deserialize remoteAuth final PersistableBundle trustCertBundle = remoteAuthBundle.getPersistableBundle(TRUST_CERT_KEY); X509Certificate caCert = null; if (trustCertBundle != null) { final byte[] encodedCaCert = PersistableBundleUtils.toByteArray(trustCertBundle); caCert = CertUtils.certificateFromByteArray(encodedCaCert); } builder.setAuthDigitalSignature(caCert, endCert, certList, privateKey); } } private static final class IkeAuthEapConfigUtils { private static final String EAP_CONFIG_KEY = "EAP_CONFIG_KEY"; @NonNull public static PersistableBundle toPersistableBundle( @NonNull IkeAuthEapConfig config, @NonNull PersistableBundle result) { result.putPersistableBundle( EAP_CONFIG_KEY, EapSessionConfigUtils.toPersistableBundle(config.getEapConfig())); return result; } public static void setBuilderByReadingPersistableBundle( @NonNull PersistableBundle localAuthBundle, @NonNull PersistableBundle remoteAuthBundle, @NonNull IkeSessionParams.Builder builder) { // Deserialize localAuth final PersistableBundle eapBundle = localAuthBundle.getPersistableBundle(EAP_CONFIG_KEY); Objects.requireNonNull(eapBundle, "EAP Config was null"); final EapSessionConfig eapConfig = EapSessionConfigUtils.fromPersistableBundle(eapBundle); // Deserialize remoteAuth final PersistableBundle trustCertBundle = remoteAuthBundle.getPersistableBundle( IkeAuthDigitalSignConfigUtils.TRUST_CERT_KEY); X509Certificate serverCaCert = null; if (trustCertBundle != null) { final byte[] encodedCaCert = PersistableBundleUtils.toByteArray(trustCertBundle); serverCaCert = CertUtils.certificateFromByteArray(encodedCaCert); } builder.setAuthEap(serverCaCert, eapConfig); } } private static final class ConfigRequest { private static final int IPV4_P_CSCF_ADDRESS = 1; private static final int IPV6_P_CSCF_ADDRESS = 2; private static final String TYPE_KEY = "type"; private static final String ADDRESS_KEY = "address"; public final int type; // Null when it is an empty request @Nullable public final InetAddress address; ConfigRequest(IkeConfigRequest config) { if (config instanceof ConfigRequestIpv4PcscfServer) { type = IPV4_P_CSCF_ADDRESS; address = ((ConfigRequestIpv4PcscfServer) config).getAddress(); } else if (config instanceof ConfigRequestIpv6PcscfServer) { type = IPV6_P_CSCF_ADDRESS; address = ((ConfigRequestIpv6PcscfServer) config).getAddress(); } else { throw new IllegalStateException("Unknown TunnelModeChildConfigRequest"); } } ConfigRequest(PersistableBundle in) { Objects.requireNonNull(in, "PersistableBundle was null"); type = in.getInt(TYPE_KEY); String addressStr = in.getString(ADDRESS_KEY); if (addressStr == null) { address = null; } else { address = InetAddresses.parseNumericAddress(addressStr); } } @NonNull public PersistableBundle toPersistableBundle() { final PersistableBundle result = new PersistableBundle(); result.putInt(TYPE_KEY, type); if (address != null) { result.putString(ADDRESS_KEY, address.getHostAddress()); } return result; } } }