1package com.android.hotspot2.omadm;
2
3import android.util.Base64;
4import android.util.Log;
5
6import com.android.anqp.eap.EAP;
7import com.android.anqp.eap.EAPMethod;
8import com.android.anqp.eap.ExpandedEAPMethod;
9import com.android.anqp.eap.InnerAuthEAP;
10import com.android.anqp.eap.NonEAPInnerAuth;
11import com.android.hotspot2.IMSIParameter;
12import com.android.hotspot2.Utils;
13import com.android.hotspot2.osu.OSUManager;
14import com.android.hotspot2.osu.commands.MOData;
15import com.android.hotspot2.pps.Credential;
16import com.android.hotspot2.pps.HomeSP;
17import com.android.hotspot2.pps.Policy;
18import com.android.hotspot2.pps.SubscriptionParameters;
19import com.android.hotspot2.pps.UpdateInfo;
20
21import org.xml.sax.SAXException;
22
23import java.io.BufferedInputStream;
24import java.io.BufferedOutputStream;
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.FileNotFoundException;
28import java.io.FileOutputStream;
29import java.io.IOException;
30import java.nio.charset.StandardCharsets;
31import java.text.DateFormat;
32import java.text.ParseException;
33import java.text.SimpleDateFormat;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.Date;
39import java.util.HashMap;
40import java.util.HashSet;
41import java.util.LinkedList;
42import java.util.List;
43import java.util.Map;
44import java.util.Set;
45import java.util.TimeZone;
46
47/**
48 * Handles provisioning of PerProviderSubscription data.
49 */
50public 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