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