blob: 9bcfddbb665e8f780baecc4e2cd52bf316aea275 [file] [log] [blame]
package com.android.server.wifi.configparse;
import android.content.Context;
import android.net.Uri;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
import android.provider.DocumentsContract;
import android.util.Base64;
import android.util.Log;
import com.android.server.wifi.IMSIParameter;
import com.android.server.wifi.anqp.eap.AuthParam;
import com.android.server.wifi.anqp.eap.EAP;
import com.android.server.wifi.anqp.eap.EAPMethod;
import com.android.server.wifi.anqp.eap.NonEAPInnerAuth;
import com.android.server.wifi.hotspot2.omadm.PasspointManagementObjectManager;
import com.android.server.wifi.hotspot2.pps.Credential;
import com.android.server.wifi.hotspot2.pps.HomeSP;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
public class ConfigBuilder {
public static final String WifiConfigType = "application/x-wifi-config";
private static final String ProfileTag = "application/x-passpoint-profile";
private static final String KeyTag = "application/x-pkcs12";
private static final String CATag = "application/x-x509-ca-cert";
private static final String X509 = "X.509";
private static final String TAG = "WCFG";
public static WifiConfiguration buildConfig(String uriString, byte[] data, Context context)
throws IOException, GeneralSecurityException, SAXException {
Log.d(TAG, "Content: " + (data != null ? data.length : -1));
byte[] b64 = Base64.decode(new String(data, StandardCharsets.ISO_8859_1), Base64.DEFAULT);
Log.d(TAG, "Decoded: " + b64.length + " bytes.");
dropFile(Uri.parse(uriString), context);
MIMEContainer mimeContainer = new
MIMEContainer(new LineNumberReader(
new InputStreamReader(new ByteArrayInputStream(b64), StandardCharsets.ISO_8859_1)),
null);
if (!mimeContainer.isBase64()) {
throw new IOException("Encoding for " +
mimeContainer.getContentType() + " is not base64");
}
MIMEContainer inner;
if (mimeContainer.getContentType().equals(WifiConfigType)) {
byte[] wrappedContent = Base64.decode(mimeContainer.getText(), Base64.DEFAULT);
Log.d(TAG, "Building container from '" +
new String(wrappedContent, StandardCharsets.ISO_8859_1) + "'");
inner = new MIMEContainer(new LineNumberReader(
new InputStreamReader(new ByteArrayInputStream(wrappedContent),
StandardCharsets.ISO_8859_1)), null);
}
else {
inner = mimeContainer;
}
return parse(inner);
}
private static void dropFile(Uri uri, Context context) {
if (DocumentsContract.isDocumentUri(context, uri)) {
DocumentsContract.deleteDocument(context.getContentResolver(), uri);
} else {
context.getContentResolver().delete(uri, null, null);
}
}
private static WifiConfiguration parse(MIMEContainer root)
throws IOException, GeneralSecurityException, SAXException {
if (root.getMimeContainers() == null) {
throw new IOException("Malformed MIME content: not multipart");
}
String moText = null;
X509Certificate caCert = null;
PrivateKey clientKey = null;
List<X509Certificate> clientChain = null;
for (MIMEContainer subContainer : root.getMimeContainers()) {
Log.d(TAG, " + Content Type: " + subContainer.getContentType());
switch (subContainer.getContentType()) {
case ProfileTag:
if (subContainer.isBase64()) {
byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT);
moText = new String(octets, StandardCharsets.UTF_8);
} else {
moText = subContainer.getText();
}
Log.d(TAG, "OMA: " + moText);
break;
case CATag: {
if (!subContainer.isBase64()) {
throw new IOException("Can't read non base64 encoded cert");
}
byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT);
CertificateFactory factory = CertificateFactory.getInstance(X509);
caCert = (X509Certificate) factory.generateCertificate(
new ByteArrayInputStream(octets));
Log.d(TAG, "Cert subject " + caCert.getSubjectX500Principal());
Log.d(TAG, "Full Cert: " + caCert);
break;
}
case KeyTag: {
if (!subContainer.isBase64()) {
throw new IOException("Can't read non base64 encoded key");
}
byte[] octets = Base64.decode(subContainer.getText(), Base64.DEFAULT);
KeyStore ks = KeyStore.getInstance("PKCS12");
ByteArrayInputStream in = new ByteArrayInputStream(octets);
ks.load(in, new char[0]);
in.close();
Log.d(TAG, "---- Start PKCS12 info " + octets.length + ", size " + ks.size());
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
clientKey = (PrivateKey) ks.getKey(alias, null);
Log.d(TAG, "Key: " + clientKey.getFormat());
Certificate[] chain = ks.getCertificateChain(alias);
if (chain != null) {
clientChain = new ArrayList<>();
for (Certificate certificate : chain) {
if (!(certificate instanceof X509Certificate)) {
Log.w(TAG, "Element in cert chain is not an X509Certificate: " +
certificate.getClass());
}
clientChain.add((X509Certificate) certificate);
}
Log.d(TAG, "Chain: " + clientChain.size());
}
}
Log.d(TAG, "---- End PKCS12 info.");
break;
}
}
}
if (moText == null) {
throw new IOException("Missing profile");
}
HomeSP homeSP = PasspointManagementObjectManager.buildSP(moText);
return buildConfig(homeSP, caCert, clientChain, clientKey);
}
private static WifiConfiguration buildConfig(HomeSP homeSP, X509Certificate caCert,
List<X509Certificate> clientChain, PrivateKey key)
throws IOException, GeneralSecurityException {
WifiConfiguration config;
EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID();
switch (eapMethodID) {
case EAP_TTLS:
if (key != null || clientChain != null) {
Log.w(TAG, "Client cert and/or key unnecessarily included with EAP-TTLS "+
"profile");
}
config = buildTTLSConfig(homeSP, caCert);
break;
case EAP_TLS:
config = buildTLSConfig(homeSP, clientChain, key, caCert);
break;
case EAP_AKA:
case EAP_AKAPrim:
case EAP_SIM:
if (key != null || clientChain != null || caCert != null) {
Log.i(TAG, "Client/CA cert and/or key unnecessarily included with " +
eapMethodID + " profile");
}
config = buildSIMConfig(homeSP);
break;
default:
throw new IOException("Unsupported EAP Method: " + eapMethodID);
}
return config;
}
// Retain for debugging purposes
/*
private static void xIterateCerts(KeyStore ks, X509Certificate caCert)
throws GeneralSecurityException {
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
Certificate cert = ks.getCertificate(alias);
Log.d("HS2J", "Checking " + alias);
if (cert instanceof X509Certificate) {
X509Certificate x509Certificate = (X509Certificate) cert;
boolean sm = x509Certificate.getSubjectX500Principal().equals(
caCert.getSubjectX500Principal());
boolean eq = false;
if (sm) {
eq = Arrays.equals(x509Certificate.getEncoded(), caCert.getEncoded());
}
Log.d("HS2J", "Subject: " + x509Certificate.getSubjectX500Principal() +
": " + sm + "/" + eq);
}
}
}
*/
private static void setAnonymousIdentityToNaiRealm(
WifiConfiguration config, Credential credential) {
/**
* Set WPA supplicant's anonymous identity field to a string containing the NAI realm, so
* that this value will be sent to the EAP server as part of the EAP-Response/ Identity
* packet. WPA supplicant will reset this field after using it for the EAP-Response/Identity
* packet, and revert to using the (real) identity field for subsequent transactions that
* request an identity (e.g. in EAP-TTLS).
*
* This NAI realm value (the portion of the identity after the '@') is used to tell the
* AAA server which AAA/H to forward packets to. The hardcoded username, "anonymous", is a
* placeholder that is not used--it is set to this value by convention. See Section 5.1 of
* RFC3748 for more details.
*
* NOTE: we do not set this value for EAP-SIM/AKA/AKA', since the EAP server expects the
* EAP-Response/Identity packet to contain an actual, IMSI-based identity, in order to
* identify the device.
*/
config.enterpriseConfig.setAnonymousIdentity("anonymous@" + credential.getRealm());
}
private static WifiConfiguration buildTTLSConfig(HomeSP homeSP, X509Certificate caCert)
throws IOException {
Credential credential = homeSP.getCredential();
if (credential.getUserName() == null || credential.getPassword() == null) {
throw new IOException("EAP-TTLS provisioned without user name or password");
}
EAPMethod eapMethod = credential.getEAPMethod();
AuthParam authParam = eapMethod.getAuthParam();
if (authParam == null ||
authParam.getAuthInfoID() != EAP.AuthInfoID.NonEAPInnerAuthType) {
throw new IOException("Bad auth parameter for EAP-TTLS: " + authParam);
}
WifiConfiguration config = buildBaseConfiguration(homeSP);
NonEAPInnerAuth ttlsParam = (NonEAPInnerAuth) authParam;
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
enterpriseConfig.setPhase2Method(remapInnerMethod(ttlsParam.getType()));
enterpriseConfig.setIdentity(credential.getUserName());
enterpriseConfig.setPassword(credential.getPassword());
enterpriseConfig.setCaCertificate(caCert);
setAnonymousIdentityToNaiRealm(config, credential);
return config;
}
private static WifiConfiguration buildTLSConfig(HomeSP homeSP,
List<X509Certificate> clientChain,
PrivateKey clientKey,
X509Certificate caCert)
throws IOException, GeneralSecurityException {
Credential credential = homeSP.getCredential();
X509Certificate clientCertificate = null;
if (clientKey == null || clientChain == null) {
throw new IOException("No key and/or cert passed for EAP-TLS");
}
if (credential.getCertType() != Credential.CertType.x509v3) {
throw new IOException("Invalid certificate type for TLS: " +
credential.getCertType());
}
byte[] reference = credential.getFingerPrint();
MessageDigest digester = MessageDigest.getInstance("SHA-256");
for (X509Certificate certificate : clientChain) {
digester.reset();
byte[] fingerprint = digester.digest(certificate.getEncoded());
if (Arrays.equals(reference, fingerprint)) {
clientCertificate = certificate;
break;
}
}
if (clientCertificate == null) {
throw new IOException("No certificate in chain matches supplied fingerprint");
}
String alias = Base64.encodeToString(reference, Base64.DEFAULT);
WifiConfiguration config = buildBaseConfiguration(homeSP);
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
enterpriseConfig.setClientCertificateAlias(alias);
enterpriseConfig.setClientKeyEntry(clientKey, clientCertificate);
enterpriseConfig.setCaCertificate(caCert);
setAnonymousIdentityToNaiRealm(config, credential);
return config;
}
private static WifiConfiguration buildSIMConfig(HomeSP homeSP)
throws IOException {
Credential credential = homeSP.getCredential();
IMSIParameter credImsi = credential.getImsi();
/*
* Uncomment to enforce strict IMSI matching with currently installed SIM cards.
*
TelephonyManager tm = TelephonyManager.from(context);
SubscriptionManager sub = SubscriptionManager.from(context);
boolean match = false;
for (int subId : sub.getActiveSubscriptionIdList()) {
String imsi = tm.getSubscriberId(subId);
if (credImsi.matches(imsi)) {
match = true;
break;
}
}
if (!match) {
throw new IOException("Supplied IMSI does not match any SIM card");
}
*/
WifiConfiguration config = buildBaseConfiguration(homeSP);
config.enterpriseConfig.setPlmn(credImsi.toString());
return config;
}
private static WifiConfiguration buildBaseConfiguration(HomeSP homeSP) throws IOException {
EAP.EAPMethodID eapMethodID = homeSP.getCredential().getEAPMethod().getEAPMethodID();
WifiConfiguration config = new WifiConfiguration();
config.FQDN = homeSP.getFQDN();
HashSet<Long> roamingConsortiumIds = homeSP.getRoamingConsortiums();
config.roamingConsortiumIds = new long[roamingConsortiumIds.size()];
int i = 0;
for (long id : roamingConsortiumIds) {
config.roamingConsortiumIds[i] = id;
i++;
}
config.providerFriendlyName = homeSP.getFriendlyName();
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
enterpriseConfig.setEapMethod(remapEAPMethod(eapMethodID));
enterpriseConfig.setRealm(homeSP.getCredential().getRealm());
config.enterpriseConfig = enterpriseConfig;
// The framework based config builder only ever builds r1 configs:
config.updateIdentifier = null;
return config;
}
private static int remapEAPMethod(EAP.EAPMethodID eapMethodID) throws IOException {
switch (eapMethodID) {
case EAP_TTLS:
return WifiEnterpriseConfig.Eap.TTLS;
case EAP_TLS:
return WifiEnterpriseConfig.Eap.TLS;
case EAP_SIM:
return WifiEnterpriseConfig.Eap.SIM;
case EAP_AKA:
return WifiEnterpriseConfig.Eap.AKA;
case EAP_AKAPrim:
return WifiEnterpriseConfig.Eap.AKA_PRIME;
default:
throw new IOException("Bad EAP method: " + eapMethodID);
}
}
private static int remapInnerMethod(NonEAPInnerAuth.NonEAPType type) throws IOException {
switch (type) {
case PAP:
return WifiEnterpriseConfig.Phase2.PAP;
case MSCHAP:
return WifiEnterpriseConfig.Phase2.MSCHAP;
case MSCHAPv2:
return WifiEnterpriseConfig.Phase2.MSCHAPV2;
case CHAP:
default:
throw new IOException("Inner method " + type + " not supported");
}
}
}