Home | History | Annotate | Download | only in omadm
      1 package com.android.hotspot2.omadm;
      2 
      3 import android.util.Base64;
      4 import android.util.Log;
      5 
      6 import com.android.anqp.eap.EAP;
      7 import com.android.anqp.eap.EAPMethod;
      8 import com.android.anqp.eap.ExpandedEAPMethod;
      9 import com.android.anqp.eap.InnerAuthEAP;
     10 import com.android.anqp.eap.NonEAPInnerAuth;
     11 import com.android.hotspot2.IMSIParameter;
     12 import com.android.hotspot2.Utils;
     13 import com.android.hotspot2.osu.OSUManager;
     14 import com.android.hotspot2.osu.commands.MOData;
     15 import com.android.hotspot2.pps.Credential;
     16 import com.android.hotspot2.pps.HomeSP;
     17 import com.android.hotspot2.pps.Policy;
     18 import com.android.hotspot2.pps.SubscriptionParameters;
     19 import com.android.hotspot2.pps.UpdateInfo;
     20 
     21 import org.xml.sax.SAXException;
     22 
     23 import java.io.BufferedInputStream;
     24 import java.io.BufferedOutputStream;
     25 import java.io.File;
     26 import java.io.FileInputStream;
     27 import java.io.FileNotFoundException;
     28 import java.io.FileOutputStream;
     29 import java.io.IOException;
     30 import java.nio.charset.StandardCharsets;
     31 import java.text.DateFormat;
     32 import java.text.ParseException;
     33 import java.text.SimpleDateFormat;
     34 import java.util.ArrayList;
     35 import java.util.Arrays;
     36 import java.util.Collection;
     37 import java.util.Collections;
     38 import java.util.Date;
     39 import java.util.HashMap;
     40 import java.util.HashSet;
     41 import java.util.LinkedList;
     42 import java.util.List;
     43 import java.util.Map;
     44 import java.util.Set;
     45 import java.util.TimeZone;
     46 
     47 /**
     48  * Handles provisioning of PerProviderSubscription data.
     49  */
     50 public class MOManager {
     51 
     52     public static final String TAG_AAAServerTrustRoot = "AAAServerTrustRoot";
     53     public static final String TAG_AbleToShare = "AbleToShare";
     54     public static final String TAG_CertificateType = "CertificateType";
     55     public static final String TAG_CertSHA256Fingerprint = "CertSHA256Fingerprint";
     56     public static final String TAG_CertURL = "CertURL";
     57     public static final String TAG_CheckAAAServerCertStatus = "CheckAAAServerCertStatus";
     58     public static final String TAG_Country = "Country";
     59     public static final String TAG_CreationDate = "CreationDate";
     60     public static final String TAG_Credential = "Credential";
     61     public static final String TAG_CredentialPriority = "CredentialPriority";
     62     public static final String TAG_DataLimit = "DataLimit";
     63     public static final String TAG_DigitalCertificate = "DigitalCertificate";
     64     public static final String TAG_DLBandwidth = "DLBandwidth";
     65     public static final String TAG_EAPMethod = "EAPMethod";
     66     public static final String TAG_EAPType = "EAPType";
     67     public static final String TAG_ExpirationDate = "ExpirationDate";
     68     public static final String TAG_Extension = "Extension";
     69     public static final String TAG_FQDN = "FQDN";
     70     public static final String TAG_FQDN_Match = "FQDN_Match";
     71     public static final String TAG_FriendlyName = "FriendlyName";
     72     public static final String TAG_HESSID = "HESSID";
     73     public static final String TAG_HomeOI = "HomeOI";
     74     public static final String TAG_HomeOIList = "HomeOIList";
     75     public static final String TAG_HomeOIRequired = "HomeOIRequired";
     76     public static final String TAG_HomeSP = "HomeSP";
     77     public static final String TAG_IconURL = "IconURL";
     78     public static final String TAG_IMSI = "IMSI";
     79     public static final String TAG_InnerEAPType = "InnerEAPType";
     80     public static final String TAG_InnerMethod = "InnerMethod";
     81     public static final String TAG_InnerVendorID = "InnerVendorID";
     82     public static final String TAG_InnerVendorType = "InnerVendorType";
     83     public static final String TAG_IPProtocol = "IPProtocol";
     84     public static final String TAG_MachineManaged = "MachineManaged";
     85     public static final String TAG_MaximumBSSLoadValue = "MaximumBSSLoadValue";
     86     public static final String TAG_MinBackhaulThreshold = "MinBackhaulThreshold";
     87     public static final String TAG_NetworkID = "NetworkID";
     88     public static final String TAG_NetworkType = "NetworkType";
     89     public static final String TAG_Other = "Other";
     90     public static final String TAG_OtherHomePartners = "OtherHomePartners";
     91     public static final String TAG_Password = "Password";
     92     public static final String TAG_PerProviderSubscription = "PerProviderSubscription";
     93     public static final String TAG_Policy = "Policy";
     94     public static final String TAG_PolicyUpdate = "PolicyUpdate";
     95     public static final String TAG_PortNumber = "PortNumber";
     96     public static final String TAG_PreferredRoamingPartnerList = "PreferredRoamingPartnerList";
     97     public static final String TAG_Priority = "Priority";
     98     public static final String TAG_Realm = "Realm";
     99     public static final String TAG_RequiredProtoPortTuple = "RequiredProtoPortTuple";
    100     public static final String TAG_Restriction = "Restriction";
    101     public static final String TAG_RoamingConsortiumOI = "RoamingConsortiumOI";
    102     public static final String TAG_SIM = "SIM";
    103     public static final String TAG_SoftTokenApp = "SoftTokenApp";
    104     public static final String TAG_SPExclusionList = "SPExclusionList";
    105     public static final String TAG_SSID = "SSID";
    106     public static final String TAG_StartDate = "StartDate";
    107     public static final String TAG_SubscriptionParameters = "SubscriptionParameters";
    108     public static final String TAG_SubscriptionUpdate = "SubscriptionUpdate";
    109     public static final String TAG_TimeLimit = "TimeLimit";
    110     public static final String TAG_TrustRoot = "TrustRoot";
    111     public static final String TAG_TypeOfSubscription = "TypeOfSubscription";
    112     public static final String TAG_ULBandwidth = "ULBandwidth";
    113     public static final String TAG_UpdateIdentifier = "UpdateIdentifier";
    114     public static final String TAG_UpdateInterval = "UpdateInterval";
    115     public static final String TAG_UpdateMethod = "UpdateMethod";
    116     public static final String TAG_URI = "URI";
    117     public static final String TAG_UsageLimits = "UsageLimits";
    118     public static final String TAG_UsageTimePeriod = "UsageTimePeriod";
    119     public static final String TAG_Username = "Username";
    120     public static final String TAG_UsernamePassword = "UsernamePassword";
    121     public static final String TAG_VendorId = "VendorId";
    122     public static final String TAG_VendorType = "VendorType";
    123 
    124     public static final long IntervalFactor = 60000L;  // All MO intervals are in minutes
    125 
    126     private static final DateFormat DTFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    127 
    128     private static final Map<String, Map<String, Object>> sSelectionMap;
    129 
    130     static {
    131         DTFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    132 
    133         sSelectionMap = new HashMap<>();
    134 
    135         setSelections(TAG_FQDN_Match,
    136                 "exactmatch", Boolean.FALSE,
    137                 "includesubdomains", Boolean.TRUE);
    138         setSelections(TAG_UpdateMethod,
    139                 "oma-dm-clientinitiated", Boolean.FALSE,
    140                 "spp-clientinitiated", Boolean.TRUE);
    141         setSelections(TAG_Restriction,
    142                 "homesp", UpdateInfo.UpdateRestriction.HomeSP,
    143                 "roamingpartner", UpdateInfo.UpdateRestriction.RoamingPartner,
    144                 "unrestricted", UpdateInfo.UpdateRestriction.Unrestricted);
    145     }
    146 
    147     private static void setSelections(String key, Object... pairs) {
    148         Map<String, Object> kvp = new HashMap<>();
    149         sSelectionMap.put(key, kvp);
    150         for (int n = 0; n < pairs.length; n += 2) {
    151             kvp.put(pairs[n].toString(), pairs[n + 1]);
    152         }
    153     }
    154 
    155     private final File mPpsFile;
    156     private final boolean mEnabled;
    157     private final Map<String, HomeSP> mSPs;
    158 
    159     public MOManager(File ppsFile, boolean hs2enabled) {
    160         mPpsFile = ppsFile;
    161         mEnabled = hs2enabled;
    162         mSPs = new HashMap<>();
    163     }
    164 
    165     public File getPpsFile() {
    166         return mPpsFile;
    167     }
    168 
    169     public boolean isEnabled() {
    170         return mEnabled;
    171     }
    172 
    173     public boolean isConfigured() {
    174         return mEnabled && !mSPs.isEmpty();
    175     }
    176 
    177     public Map<String, HomeSP> getLoadedSPs() {
    178         return Collections.unmodifiableMap(mSPs);
    179     }
    180 
    181     public List<HomeSP> loadAllSPs() throws IOException {
    182 
    183         if (!mEnabled || !mPpsFile.exists()) {
    184             return Collections.emptyList();
    185         }
    186 
    187         try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
    188             MOTree moTree = MOTree.unmarshal(in);
    189             mSPs.clear();
    190             if (moTree == null) {
    191                 return Collections.emptyList();     // Empty file
    192             }
    193 
    194             List<HomeSP> sps = buildSPs(moTree);
    195             if (sps != null) {
    196                 for (HomeSP sp : sps) {
    197                     if (mSPs.put(sp.getFQDN(), sp) != null) {
    198                         throw new OMAException("Multiple SPs for FQDN '" + sp.getFQDN() + "'");
    199                     } else {
    200                         Log.d(OSUManager.TAG, "retrieved " + sp.getFQDN() + " from PPS");
    201                     }
    202                 }
    203                 return sps;
    204 
    205             } else {
    206                 throw new OMAException("Failed to build HomeSP");
    207             }
    208         }
    209     }
    210 
    211     public static HomeSP buildSP(String xml) throws IOException, SAXException {
    212         OMAParser omaParser = new OMAParser();
    213         MOTree tree = omaParser.parse(xml, OMAConstants.PPS_URN);
    214         List<HomeSP> spList = buildSPs(tree);
    215         if (spList.size() != 1) {
    216             throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
    217         }
    218         return spList.iterator().next();
    219     }
    220 
    221     public HomeSP addSP(String xml, OSUManager osuManager) throws IOException, SAXException {
    222         OMAParser omaParser = new OMAParser();
    223         return addSP(omaParser.parse(xml, OMAConstants.PPS_URN), osuManager);
    224     }
    225 
    226     private static final List<String> FQDNPath = Arrays.asList(TAG_HomeSP, TAG_FQDN);
    227 
    228     /**
    229      * R1 *only* addSP method.
    230      *
    231      * @param homeSP
    232      * @throws IOException
    233      */
    234     public void addSP(HomeSP homeSP, OSUManager osuManager) throws IOException {
    235         if (!mEnabled) {
    236             throw new IOException("HS2.0 not enabled on this device");
    237         }
    238         if (mSPs.containsKey(homeSP.getFQDN())) {
    239             Log.d(OSUManager.TAG, "HS20 profile for " +
    240                     homeSP.getFQDN() + " already exists");
    241             return;
    242         }
    243         Log.d(OSUManager.TAG, "Adding new HS20 profile for " + homeSP.getFQDN());
    244 
    245         OMAConstructed dummyRoot = new OMAConstructed(null, TAG_PerProviderSubscription, null);
    246         buildHomeSPTree(homeSP, dummyRoot, mSPs.size() + 1);
    247         try {
    248             addSP(dummyRoot, osuManager);
    249         } catch (FileNotFoundException fnfe) {
    250             MOTree tree =
    251                     MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
    252             // No file to load a pre-build MO tree from, create a new one and save it.
    253             //MOTree tree = new MOTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, dummyRoot);
    254             writeMO(tree, mPpsFile, osuManager);
    255         }
    256         mSPs.put(homeSP.getFQDN(), homeSP);
    257     }
    258 
    259     public HomeSP addSP(MOTree instanceTree, OSUManager osuManager) throws IOException {
    260         List<HomeSP> spList = buildSPs(instanceTree);
    261         if (spList.size() != 1) {
    262             throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
    263         }
    264 
    265         HomeSP sp = spList.iterator().next();
    266         String fqdn = sp.getFQDN();
    267         if (mSPs.put(fqdn, sp) != null) {
    268             throw new OMAException("SP " + fqdn + " already exists");
    269         }
    270 
    271         OMAConstructed pps = (OMAConstructed) instanceTree.getRoot().
    272                 getChild(TAG_PerProviderSubscription);
    273 
    274         try {
    275             addSP(pps, osuManager);
    276         } catch (FileNotFoundException fnfe) {
    277             MOTree tree = new MOTree(instanceTree.getUrn(), instanceTree.getDtdRev(),
    278                     instanceTree.getRoot());
    279             writeMO(tree, mPpsFile, osuManager);
    280         }
    281 
    282         return sp;
    283     }
    284 
    285     /**
    286      * Add an SP sub-tree. mo must be PPS with an immediate instance child (e.g. Cred01) and an
    287      * optional UpdateIdentifier,
    288      *
    289      * @param mo The new MO
    290      * @throws IOException
    291      */
    292     private void addSP(OMANode mo, OSUManager osuManager) throws IOException {
    293         MOTree moTree;
    294         try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
    295             moTree = MOTree.unmarshal(in);
    296             moTree.getRoot().addChild(mo);
    297 
    298                 /*
    299             OMAConstructed ppsRoot = (OMAConstructed)
    300                     moTree.getRoot().addChild(TAG_PerProviderSubscription, "", null, null);
    301             for (OMANode child : mo.getChildren()) {
    302                 ppsRoot.addChild(child);
    303                 if (!child.isLeaf()) {
    304                     moTree.getRoot().addChild(child);
    305                 }
    306                 else if (child.getName().equals(TAG_UpdateIdentifier)) {
    307                     OMANode currentUD = moTree.getRoot().getChild(TAG_UpdateIdentifier);
    308                     if (currentUD != null) {
    309                         moTree.getRoot().replaceNode(currentUD, child);
    310                     }
    311                     else {
    312                         moTree.getRoot().addChild(child);
    313                     }
    314                 }
    315             }
    316                 */
    317         }
    318         writeMO(moTree, mPpsFile, osuManager);
    319     }
    320 
    321     private static OMAConstructed findTargetTree(MOTree moTree, String fqdn) throws OMAException {
    322         OMANode pps = moTree.getRoot();
    323         for (OMANode node : pps.getChildren()) {
    324             OMANode instance = null;
    325             if (node.getName().equals(TAG_PerProviderSubscription)) {
    326                 instance = getInstanceNode((OMAConstructed) node);
    327             } else if (!node.isLeaf()) {
    328                 instance = node;
    329             }
    330             if (instance != null) {
    331                 String nodeFqdn = getString(instance.getListValue(FQDNPath.iterator()));
    332                 if (fqdn.equalsIgnoreCase(nodeFqdn)) {
    333                     return (OMAConstructed) node;
    334                     // targetTree is rooted at the PPS
    335                 }
    336             }
    337         }
    338         return null;
    339     }
    340 
    341     private static OMAConstructed getInstanceNode(OMAConstructed root) throws OMAException {
    342         for (OMANode child : root.getChildren()) {
    343             if (!child.isLeaf()) {
    344                 return (OMAConstructed) child;
    345             }
    346         }
    347         throw new OMAException("Cannot find instance node");
    348     }
    349 
    350     public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods, OSUManager osuManager)
    351             throws IOException {
    352 
    353         Log.d(OSUManager.TAG, "modifying SP: " + mods);
    354         MOTree moTree;
    355         int ppsMods = 0;
    356         int updateIdentifier = 0;
    357         try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
    358             moTree = MOTree.unmarshal(in);
    359             // moTree is PPS/?/provider-data
    360 
    361             OMAConstructed targetTree = findTargetTree(moTree, homeSP.getFQDN());
    362             if (targetTree == null) {
    363                 throw new IOException("Failed to find PPS tree for " + homeSP.getFQDN());
    364             }
    365             OMAConstructed instance = getInstanceNode(targetTree);
    366 
    367             for (MOData mod : mods) {
    368                 LinkedList<String> tailPath =
    369                         getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
    370                 OMAConstructed modRoot = mod.getMOTree().getRoot();
    371                 // modRoot is the MgmtTree with the actual object as a direct child
    372                 // (e.g. Credential)
    373 
    374                 if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
    375                     updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
    376                     OMANode oldUdi = targetTree.getChild(TAG_UpdateIdentifier);
    377                     if (getInteger(oldUdi) != updateIdentifier) {
    378                         ppsMods++;
    379                     }
    380                     if (oldUdi != null) {
    381                         targetTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
    382                     } else {
    383                         targetTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
    384                     }
    385                 } else {
    386                     tailPath.removeFirst();     // Drop the instance
    387                     OMANode current = instance.getListValue(tailPath.iterator());
    388                     if (current == null) {
    389                         throw new IOException("No previous node for " + tailPath + " in " +
    390                                 homeSP.getFQDN());
    391                     }
    392                     for (OMANode newNode : modRoot.getChildren()) {
    393                         // newNode is something like Credential
    394                         // current is the same existing node
    395                         OMANode old = current.getParent().replaceNode(current, newNode);
    396                         ppsMods++;
    397                     }
    398                 }
    399             }
    400         }
    401         writeMO(moTree, mPpsFile, osuManager);
    402 
    403         if (ppsMods == 0) {
    404             return null;    // HomeSP not modified.
    405         }
    406 
    407         // Return a new rebuilt HomeSP
    408         List<HomeSP> sps = buildSPs(moTree);
    409         if (sps != null) {
    410             for (HomeSP sp : sps) {
    411                 if (sp.getFQDN().equals(homeSP.getFQDN())) {
    412                     return sp;
    413                 }
    414             }
    415         } else {
    416             throw new OMAException("Failed to build HomeSP");
    417         }
    418         return null;
    419     }
    420 
    421     private static LinkedList<String> getTailPath(String pathString, String rootName)
    422             throws IOException {
    423         String[] path = pathString.split("/");
    424         int pathIndex;
    425         for (pathIndex = 0; pathIndex < path.length; pathIndex++) {
    426             if (path[pathIndex].equalsIgnoreCase(rootName)) {
    427                 pathIndex++;
    428                 break;
    429             }
    430         }
    431         if (pathIndex >= path.length) {
    432             throw new IOException("Bad node-path: " + pathString);
    433         }
    434         LinkedList<String> tailPath = new LinkedList<>();
    435         while (pathIndex < path.length) {
    436             tailPath.add(path[pathIndex]);
    437             pathIndex++;
    438         }
    439         return tailPath;
    440     }
    441 
    442     public HomeSP getHomeSP(String fqdn) {
    443         return mSPs.get(fqdn);
    444     }
    445 
    446     public void removeSP(String fqdn, OSUManager osuManager) throws IOException {
    447         if (mSPs.remove(fqdn) == null) {
    448             Log.d(OSUManager.TAG, "No HS20 profile to delete for " + fqdn);
    449             return;
    450         }
    451 
    452         Log.d(OSUManager.TAG, "Deleting HS20 profile for " + fqdn);
    453 
    454         MOTree moTree;
    455         try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
    456             moTree = MOTree.unmarshal(in);
    457             OMAConstructed tbd = findTargetTree(moTree, fqdn);
    458             if (tbd == null) {
    459                 throw new IOException("Node " + fqdn + " doesn't exist in MO tree");
    460             }
    461             OMAConstructed pps = moTree.getRoot();
    462             OMANode removed = pps.removeNode("?", tbd);
    463             if (removed == null) {
    464                 throw new IOException("Failed to remove " + fqdn + " out of MO tree");
    465             }
    466         }
    467         writeMO(moTree, mPpsFile, osuManager);
    468         osuManager.spDeleted(fqdn);
    469     }
    470 
    471     public MOTree getMOTree(HomeSP homeSP) throws IOException {
    472         try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
    473             MOTree moTree = MOTree.unmarshal(in);
    474             OMAConstructed target = findTargetTree(moTree, homeSP.getFQDN());
    475             if (target == null) {
    476                 throw new IOException("Can't find " + homeSP.getFQDN() + " in MO tree");
    477             }
    478             return MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, target);
    479         }
    480     }
    481 
    482     private static void writeMO(MOTree moTree, File f, OSUManager osuManager) throws IOException {
    483         try (BufferedOutputStream out =
    484                      new BufferedOutputStream(new FileOutputStream(f, false))) {
    485             moTree.marshal(out);
    486             out.flush();
    487         }
    488     }
    489 
    490     private static String fqdnList(Collection<HomeSP> sps) {
    491         StringBuilder sb = new StringBuilder();
    492         boolean first = true;
    493         for (HomeSP sp : sps) {
    494             if (first) {
    495                 first = false;
    496             } else {
    497                 sb.append(", ");
    498             }
    499             sb.append(sp.getFQDN());
    500         }
    501         return sb.toString();
    502     }
    503 
    504     private static OMANode buildHomeSPTree(HomeSP homeSP, OMAConstructed root, int instanceID)
    505             throws IOException {
    506         OMANode providerSubNode = root.addChild(getInstanceString(instanceID),
    507                 null, null, null);
    508 
    509         // The HomeSP:
    510         OMANode homeSpNode = providerSubNode.addChild(TAG_HomeSP, null, null, null);
    511         if (!homeSP.getSSIDs().isEmpty()) {
    512             OMAConstructed nwkIDNode =
    513                     (OMAConstructed) homeSpNode.addChild(TAG_NetworkID, null, null, null);
    514             int instance = 0;
    515             for (Map.Entry<String, Long> entry : homeSP.getSSIDs().entrySet()) {
    516                 OMAConstructed inode =
    517                         (OMAConstructed) nwkIDNode
    518                                 .addChild(getInstanceString(instance++), null, null, null);
    519                 inode.addChild(TAG_SSID, null, entry.getKey(), null);
    520                 if (entry.getValue() != null) {
    521                     inode.addChild(TAG_HESSID, null,
    522                             String.format("%012x", entry.getValue()), null);
    523                 }
    524             }
    525         }
    526 
    527         homeSpNode.addChild(TAG_FriendlyName, null, homeSP.getFriendlyName(), null);
    528 
    529         if (homeSP.getIconURL() != null) {
    530             homeSpNode.addChild(TAG_IconURL, null, homeSP.getIconURL(), null);
    531         }
    532 
    533         homeSpNode.addChild(TAG_FQDN, null, homeSP.getFQDN(), null);
    534 
    535         if (!homeSP.getMatchAllOIs().isEmpty() || !homeSP.getMatchAnyOIs().isEmpty()) {
    536             OMAConstructed homeOIList =
    537                     (OMAConstructed) homeSpNode.addChild(TAG_HomeOIList, null, null, null);
    538 
    539             int instance = 0;
    540             for (Long oi : homeSP.getMatchAllOIs()) {
    541                 OMAConstructed inode =
    542                         (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
    543                                 null, null, null);
    544                 inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
    545                 inode.addChild(TAG_HomeOIRequired, null, "TRUE", null);
    546             }
    547             for (Long oi : homeSP.getMatchAnyOIs()) {
    548                 OMAConstructed inode =
    549                         (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
    550                                 null, null, null);
    551                 inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
    552                 inode.addChild(TAG_HomeOIRequired, null, "FALSE", null);
    553             }
    554         }
    555 
    556         if (!homeSP.getOtherHomePartners().isEmpty()) {
    557             OMAConstructed otherPartners =
    558                     (OMAConstructed) homeSpNode.addChild(TAG_OtherHomePartners, null, null, null);
    559             int instance = 0;
    560             for (String fqdn : homeSP.getOtherHomePartners()) {
    561                 OMAConstructed inode =
    562                         (OMAConstructed) otherPartners.addChild(getInstanceString(instance++),
    563                                 null, null, null);
    564                 inode.addChild(TAG_FQDN, null, fqdn, null);
    565             }
    566         }
    567 
    568         if (!homeSP.getRoamingConsortiums().isEmpty()) {
    569             homeSpNode.addChild(TAG_RoamingConsortiumOI, null,
    570                     getRCList(homeSP.getRoamingConsortiums()), null);
    571         }
    572 
    573         // The Credential:
    574         OMANode credentialNode = providerSubNode.addChild(TAG_Credential, null, null, null);
    575         Credential cred = homeSP.getCredential();
    576         EAPMethod method = cred.getEAPMethod();
    577 
    578         if (cred.getCtime() > 0) {
    579             credentialNode.addChild(TAG_CreationDate,
    580                     null, DTFormat.format(new Date(cred.getCtime())), null);
    581         }
    582         if (cred.getExpTime() > 0) {
    583             credentialNode.addChild(TAG_ExpirationDate,
    584                     null, DTFormat.format(new Date(cred.getExpTime())), null);
    585         }
    586 
    587         if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_SIM
    588                 || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKA
    589                 || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKAPrim) {
    590 
    591             OMANode simNode = credentialNode.addChild(TAG_SIM, null, null, null);
    592             simNode.addChild(TAG_IMSI, null, cred.getImsi().toString(), null);
    593             simNode.addChild(TAG_EAPType, null,
    594                     Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
    595 
    596         } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TTLS) {
    597 
    598             OMANode unpNode = credentialNode.addChild(TAG_UsernamePassword, null, null, null);
    599             unpNode.addChild(TAG_Username, null, cred.getUserName(), null);
    600             unpNode.addChild(TAG_Password, null,
    601                     Base64.encodeToString(cred.getPassword().getBytes(StandardCharsets.UTF_8),
    602                             Base64.DEFAULT), null);
    603             OMANode eapNode = unpNode.addChild(TAG_EAPMethod, null, null, null);
    604             eapNode.addChild(TAG_EAPType, null,
    605                     Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
    606             eapNode.addChild(TAG_InnerMethod, null,
    607                     ((NonEAPInnerAuth) method.getAuthParam()).getOMAtype(), null);
    608 
    609         } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TLS) {
    610 
    611             OMANode certNode = credentialNode.addChild(TAG_DigitalCertificate, null, null, null);
    612             certNode.addChild(TAG_CertificateType, null, Credential.CertTypeX509, null);
    613             certNode.addChild(TAG_CertSHA256Fingerprint, null,
    614                     Utils.toHex(cred.getFingerPrint()), null);
    615 
    616         } else {
    617             throw new OMAException("Invalid credential on " + homeSP.getFQDN());
    618         }
    619 
    620         credentialNode.addChild(TAG_Realm, null, cred.getRealm(), null);
    621 
    622         // !!! Note: This node defines CRL checking through OSCP, I suspect we won't be able
    623         // to do that so it is commented out:
    624         //credentialNode.addChild(TAG_CheckAAAServerCertStatus, null, "TRUE", null);
    625         return providerSubNode;
    626     }
    627 
    628     private static String getInstanceString(int instance) {
    629         return "r1i" + instance;
    630     }
    631 
    632     private static String getRCList(Collection<Long> rcs) {
    633         StringBuilder builder = new StringBuilder();
    634         boolean first = true;
    635         for (Long roamingConsortium : rcs) {
    636             if (first) {
    637                 first = false;
    638             } else {
    639                 builder.append(',');
    640             }
    641             builder.append(String.format("%x", roamingConsortium));
    642         }
    643         return builder.toString();
    644     }
    645 
    646     public static List<HomeSP> buildSPs(MOTree moTree) throws OMAException {
    647         OMAConstructed spList;
    648         List<HomeSP> homeSPs = new ArrayList<>();
    649         if (moTree.getRoot().getName().equals(TAG_PerProviderSubscription)) {
    650             // The old PPS file was rooted at PPS instead of MgmtTree to conserve space
    651             spList = moTree.getRoot();
    652 
    653             if (spList == null) {
    654                 return homeSPs;
    655             }
    656 
    657             for (OMANode node : spList.getChildren()) {
    658                 if (!node.isLeaf()) {
    659                     homeSPs.add(buildHomeSP(node, 0));
    660                 }
    661             }
    662         } else {
    663             for (OMANode ppsRoot : moTree.getRoot().getChildren()) {
    664                 if (ppsRoot.getName().equals(TAG_PerProviderSubscription)) {
    665                     Integer updateIdentifier = null;
    666                     OMANode instance = null;
    667                     for (OMANode child : ppsRoot.getChildren()) {
    668                         if (child.getName().equals(TAG_UpdateIdentifier)) {
    669                             updateIdentifier = getInteger(child);
    670                         } else if (!child.isLeaf()) {
    671                             instance = child;
    672                         }
    673                     }
    674                     if (instance == null) {
    675                         throw new OMAException("PPS node missing instance node");
    676                     }
    677                     homeSPs.add(buildHomeSP(instance,
    678                             updateIdentifier != null ? updateIdentifier : 0));
    679                 }
    680             }
    681         }
    682 
    683         return homeSPs;
    684     }
    685 
    686     private static HomeSP buildHomeSP(OMANode ppsRoot, int updateIdentifier) throws OMAException {
    687         OMANode spRoot = ppsRoot.getChild(TAG_HomeSP);
    688 
    689         String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator());
    690         String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator());
    691         String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator());
    692 
    693         HashSet<Long> roamingConsortiums = new HashSet<>();
    694         String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator());
    695         if (oiString != null) {
    696             for (String oi : oiString.split(",")) {
    697                 roamingConsortiums.add(Long.parseLong(oi.trim(), 16));
    698             }
    699         }
    700 
    701         Map<String, Long> ssids = new HashMap<>();
    702 
    703         OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator());
    704         if (ssidListNode != null) {
    705             for (OMANode ssidRoot : ssidListNode.getChildren()) {
    706                 OMANode hessidNode = ssidRoot.getChild(TAG_HESSID);
    707                 ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), getMac(hessidNode));
    708             }
    709         }
    710 
    711         Set<Long> matchAnyOIs = new HashSet<>();
    712         List<Long> matchAllOIs = new ArrayList<>();
    713         OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator());
    714         if (homeOIListNode != null) {
    715             for (OMANode homeOIRoot : homeOIListNode.getChildren()) {
    716                 String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue();
    717                 if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) {
    718                     matchAllOIs.add(Long.parseLong(homeOI, 16));
    719                 } else {
    720                     matchAnyOIs.add(Long.parseLong(homeOI, 16));
    721                 }
    722             }
    723         }
    724 
    725         Set<String> otherHomePartners = new HashSet<>();
    726         OMANode otherListNode =
    727                 spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator());
    728         if (otherListNode != null) {
    729             for (OMANode fqdnNode : otherListNode.getChildren()) {
    730                 otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue());
    731             }
    732         }
    733 
    734         Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential));
    735 
    736         OMANode policyNode = ppsRoot.getChild(TAG_Policy);
    737         Policy policy = policyNode != null ? new Policy(policyNode) : null;
    738 
    739         Map<String, String> aaaTrustRoots;
    740         OMANode aaaRootNode = ppsRoot.getChild(TAG_AAAServerTrustRoot);
    741         if (aaaRootNode == null) {
    742             aaaTrustRoots = null;
    743         } else {
    744             aaaTrustRoots = new HashMap<>(aaaRootNode.getChildren().size());
    745             for (OMANode child : aaaRootNode.getChildren()) {
    746                 aaaTrustRoots.put(getString(child, TAG_CertURL),
    747                         getString(child, TAG_CertSHA256Fingerprint));
    748             }
    749         }
    750 
    751         OMANode updateNode = ppsRoot.getChild(TAG_SubscriptionUpdate);
    752         UpdateInfo subscriptionUpdate = updateNode != null ? new UpdateInfo(updateNode) : null;
    753         OMANode subNode = ppsRoot.getChild(TAG_SubscriptionParameters);
    754         SubscriptionParameters subscriptionParameters = subNode != null ?
    755                 new SubscriptionParameters(subNode) : null;
    756 
    757         return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners,
    758                 matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential,
    759                 policy, getInteger(ppsRoot.getChild(TAG_CredentialPriority), 0),
    760                 aaaTrustRoots, subscriptionUpdate, subscriptionParameters, updateIdentifier);
    761     }
    762 
    763     private static Credential buildCredential(OMANode credNode) throws OMAException {
    764         long ctime = getTime(credNode.getChild(TAG_CreationDate));
    765         long expTime = getTime(credNode.getChild(TAG_ExpirationDate));
    766         String realm = getString(credNode.getChild(TAG_Realm));
    767         boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus));
    768 
    769         OMANode unNode = credNode.getChild(TAG_UsernamePassword);
    770         OMANode certNode = credNode.getChild(TAG_DigitalCertificate);
    771         OMANode simNode = credNode.getChild(TAG_SIM);
    772 
    773         int alternatives = 0;
    774         alternatives += unNode != null ? 1 : 0;
    775         alternatives += certNode != null ? 1 : 0;
    776         alternatives += simNode != null ? 1 : 0;
    777         if (alternatives != 1) {
    778             throw new OMAException("Expected exactly one credential type, got " + alternatives);
    779         }
    780 
    781         if (unNode != null) {
    782             String userName = getString(unNode.getChild(TAG_Username));
    783             String password = getString(unNode.getChild(TAG_Password));
    784             boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged));
    785             String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp));
    786             boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare));
    787 
    788             OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod);
    789             int eapID = getInteger(eapMethodNode.getChild(TAG_EAPType));
    790 
    791             EAP.EAPMethodID eapMethodID = EAP.mapEAPMethod(eapID);
    792             if (eapMethodID == null) {
    793                 throw new OMAException("Unknown EAP method: " + eapID);
    794             }
    795 
    796             Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId));
    797             Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType));
    798             Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType));
    799             EAP.EAPMethodID innerEAPMethod = null;
    800             if (innerEAPType != null) {
    801                 innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue());
    802                 if (innerEAPMethod == null) {
    803                     throw new OMAException("Bad inner EAP method: " + innerEAPType);
    804                 }
    805             }
    806 
    807             Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID));
    808             Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType));
    809             String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod));
    810 
    811             EAPMethod eapMethod;
    812             if (innerEAPMethod != null) {
    813                 eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod));
    814             } else if (vid != null) {
    815                 eapMethod = new EAPMethod(eapMethodID,
    816                         new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod,
    817                                 vid.intValue(), vtype));
    818             } else if (innerVid != null) {
    819                 eapMethod =
    820                         new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID
    821                                 .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype));
    822             } else if (innerNonEAPMethod != null) {
    823                 eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod));
    824             } else {
    825                 throw new OMAException("Incomplete set of EAP parameters");
    826             }
    827 
    828             return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName,
    829                     password, machineManaged, softTokenApp, ableToShare);
    830         }
    831         if (certNode != null) {
    832             try {
    833                 String certTypeString = getString(certNode.getChild(TAG_CertificateType));
    834                 byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint));
    835 
    836                 EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null);
    837 
    838                 return new Credential(ctime, expTime, realm, checkAAACert, eapMethod,
    839                         Credential.mapCertType(certTypeString), fingerPrint);
    840             } catch (NumberFormatException nfe) {
    841                 throw new OMAException("Bad hex string: " + nfe.toString());
    842             }
    843         }
    844         if (simNode != null) {
    845             try {
    846                 IMSIParameter imsi = new IMSIParameter(getString(simNode.getChild(TAG_IMSI)));
    847 
    848                 EAPMethod eapMethod =
    849                         new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))),
    850                                 null);
    851 
    852                 return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi);
    853             } catch (IOException ioe) {
    854                 throw new OMAException("Failed to parse IMSI: " + ioe);
    855             }
    856         }
    857         throw new OMAException("Missing credential parameters");
    858     }
    859 
    860     public static OMANode getChild(OMANode node, String key) throws OMAException {
    861         OMANode child = node.getChild(key);
    862         if (child == null) {
    863             throw new OMAException("No such node: " + key);
    864         }
    865         return child;
    866     }
    867 
    868     public static String getString(OMANode node, String key) throws OMAException {
    869         OMANode child = node.getChild(key);
    870         if (child == null) {
    871             throw new OMAException("Missing value for " + key);
    872         } else if (!child.isLeaf()) {
    873             throw new OMAException(key + " is not a leaf node");
    874         }
    875         return child.getValue();
    876     }
    877 
    878     public static long getLong(OMANode node, String key, Long dflt) throws OMAException {
    879         OMANode child = node.getChild(key);
    880         if (child == null) {
    881             if (dflt != null) {
    882                 return dflt;
    883             } else {
    884                 throw new OMAException("Missing value for " + key);
    885             }
    886         } else {
    887             if (!child.isLeaf()) {
    888                 throw new OMAException(key + " is not a leaf node");
    889             }
    890             String value = child.getValue();
    891             try {
    892                 long result = Long.parseLong(value);
    893                 if (result < 0) {
    894                     throw new OMAException("Negative value for " + key);
    895                 }
    896                 return result;
    897             } catch (NumberFormatException nfe) {
    898                 throw new OMAException("Value for " + key + " is non-numeric: " + value);
    899             }
    900         }
    901     }
    902 
    903     public static <T> T getSelection(OMANode node, String key) throws OMAException {
    904         OMANode child = node.getChild(key);
    905         if (child == null) {
    906             throw new OMAException("Missing value for " + key);
    907         } else if (!child.isLeaf()) {
    908             throw new OMAException(key + " is not a leaf node");
    909         }
    910         return getSelection(key, child.getValue());
    911     }
    912 
    913     public static <T> T getSelection(String key, String value) throws OMAException {
    914         if (value == null) {
    915             throw new OMAException("No value for " + key);
    916         }
    917         Map<String, Object> kvp = sSelectionMap.get(key);
    918         T result = (T) kvp.get(value.toLowerCase());
    919         if (result == null) {
    920             throw new OMAException("Invalid value '" + value + "' for " + key);
    921         }
    922         return result;
    923     }
    924 
    925     private static boolean getBoolean(OMANode boolNode) {
    926         return boolNode != null && Boolean.parseBoolean(boolNode.getValue());
    927     }
    928 
    929     public static String getString(OMANode stringNode) {
    930         return stringNode != null ? stringNode.getValue() : null;
    931     }
    932 
    933     private static int getInteger(OMANode intNode, int dflt) throws OMAException {
    934         if (intNode == null) {
    935             return dflt;
    936         }
    937         return getInteger(intNode);
    938     }
    939 
    940     private static int getInteger(OMANode intNode) throws OMAException {
    941         if (intNode == null) {
    942             throw new OMAException("Missing integer value");
    943         }
    944         try {
    945             return Integer.parseInt(intNode.getValue());
    946         } catch (NumberFormatException nfe) {
    947             throw new OMAException("Invalid integer: " + intNode.getValue());
    948         }
    949     }
    950 
    951     private static Long getMac(OMANode macNode) throws OMAException {
    952         if (macNode == null) {
    953             return null;
    954         }
    955         try {
    956             return Long.parseLong(macNode.getValue(), 16);
    957         } catch (NumberFormatException nfe) {
    958             throw new OMAException("Invalid MAC: " + macNode.getValue());
    959         }
    960     }
    961 
    962     private static Long getOptionalInteger(OMANode intNode) throws OMAException {
    963         if (intNode == null) {
    964             return null;
    965         }
    966         try {
    967             return Long.parseLong(intNode.getValue());
    968         } catch (NumberFormatException nfe) {
    969             throw new OMAException("Invalid integer: " + intNode.getValue());
    970         }
    971     }
    972 
    973     public static long getTime(OMANode timeNode) throws OMAException {
    974         if (timeNode == null) {
    975             return Utils.UNSET_TIME;
    976         }
    977         String timeText = timeNode.getValue();
    978         try {
    979             Date date = DTFormat.parse(timeText);
    980             return date.getTime();
    981         } catch (ParseException pe) {
    982             throw new OMAException("Badly formatted time: " + timeText);
    983         }
    984     }
    985 
    986     private static byte[] getOctets(OMANode octetNode) throws OMAException {
    987         if (octetNode == null) {
    988             throw new OMAException("Missing byte value");
    989         }
    990         return Utils.hexToBytes(octetNode.getValue());
    991     }
    992 }
    993