MOManager.java revision 0ff7a0a25679b1a4d483c6f750973c57b25b6826
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));
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) 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);
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);
255        }
256        mSPs.put(homeSP.getFQDN(), homeSP);
257    }
258
259    public HomeSP addSP(MOTree instanceTree) 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);
276        } catch (FileNotFoundException fnfe) {
277            MOTree tree = new MOTree(instanceTree.getUrn(), instanceTree.getDtdRev(),
278                    instanceTree.getRoot());
279            writeMO(tree, mPpsFile);
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) 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);
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 static HomeSP modifySP(HomeSP homeSP, MOTree moTree, Collection<MOData> mods)
351            throws OMAException {
352
353        OMAConstructed ppsTree =
354                (OMAConstructed) moTree.getRoot().getChildren().iterator().next();
355        OMAConstructed instance = getInstanceNode(ppsTree);
356
357        int ppsMods = 0;
358        int updateIdentifier = homeSP.getUpdateIdentifier();
359        for (MOData mod : mods) {
360            LinkedList<String> tailPath =
361                    getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
362            OMAConstructed modRoot = mod.getMOTree().getRoot();
363            // modRoot is the MgmtTree with the actual object as a direct child
364            // (e.g. Credential)
365
366            if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
367                updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
368                OMANode oldUdi = ppsTree.getChild(TAG_UpdateIdentifier);
369                if (getInteger(oldUdi) != updateIdentifier) {
370                    ppsMods++;
371                }
372                if (oldUdi != null) {
373                    ppsTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
374                } else {
375                    ppsTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
376                }
377            } else {
378                tailPath.removeFirst();     // Drop the instance
379                OMANode current = instance.getListValue(tailPath.iterator());
380                if (current == null) {
381                    throw new OMAException("No previous node for " + tailPath + " in "
382                            + homeSP.getFQDN());
383                }
384                for (OMANode newNode : modRoot.getChildren()) {
385                    // newNode is something like Credential
386                    // current is the same existing node
387                    OMANode old = current.getParent().replaceNode(current, newNode);
388                    ppsMods++;
389                }
390            }
391        }
392
393        return ppsMods > 0 ? buildHomeSP(instance, updateIdentifier) : null;
394    }
395
396    public HomeSP modifySP(HomeSP homeSP, Collection<MOData> mods)
397            throws IOException {
398
399        Log.d(OSUManager.TAG, "modifying SP: " + mods);
400        MOTree moTree;
401        int ppsMods = 0;
402        int updateIdentifier = 0;
403        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
404            moTree = MOTree.unmarshal(in);
405            // moTree is PPS/?/provider-data
406
407            OMAConstructed targetTree = findTargetTree(moTree, homeSP.getFQDN());
408            if (targetTree == null) {
409                throw new IOException("Failed to find PPS tree for " + homeSP.getFQDN());
410            }
411            OMAConstructed instance = getInstanceNode(targetTree);
412
413            for (MOData mod : mods) {
414                LinkedList<String> tailPath =
415                        getTailPath(mod.getBaseURI(), TAG_PerProviderSubscription);
416                OMAConstructed modRoot = mod.getMOTree().getRoot();
417                // modRoot is the MgmtTree with the actual object as a direct child
418                // (e.g. Credential)
419
420                if (tailPath.getFirst().equals(TAG_UpdateIdentifier)) {
421                    updateIdentifier = getInteger(modRoot.getChildren().iterator().next());
422                    OMANode oldUdi = targetTree.getChild(TAG_UpdateIdentifier);
423                    if (getInteger(oldUdi) != updateIdentifier) {
424                        ppsMods++;
425                    }
426                    if (oldUdi != null) {
427                        targetTree.replaceNode(oldUdi, modRoot.getChild(TAG_UpdateIdentifier));
428                    } else {
429                        targetTree.addChild(modRoot.getChild(TAG_UpdateIdentifier));
430                    }
431                } else {
432                    tailPath.removeFirst();     // Drop the instance
433                    OMANode current = instance.getListValue(tailPath.iterator());
434                    if (current == null) {
435                        throw new IOException("No previous node for " + tailPath + " in " +
436                                homeSP.getFQDN());
437                    }
438                    for (OMANode newNode : modRoot.getChildren()) {
439                        // newNode is something like Credential
440                        // current is the same existing node
441                        OMANode old = current.getParent().replaceNode(current, newNode);
442                        ppsMods++;
443                    }
444                }
445            }
446        }
447        writeMO(moTree, mPpsFile);
448
449        if (ppsMods == 0) {
450            return null;    // HomeSP not modified.
451        }
452
453        // Return a new rebuilt HomeSP
454        List<HomeSP> sps = buildSPs(moTree);
455        if (sps != null) {
456            for (HomeSP sp : sps) {
457                if (sp.getFQDN().equals(homeSP.getFQDN())) {
458                    return sp;
459                }
460            }
461        } else {
462            throw new OMAException("Failed to build HomeSP");
463        }
464        return null;
465    }
466
467    private static LinkedList<String> getTailPath(String pathString, String rootName)
468            throws OMAException {
469        String[] path = pathString.split("/");
470        int pathIndex;
471        for (pathIndex = 0; pathIndex < path.length; pathIndex++) {
472            if (path[pathIndex].equalsIgnoreCase(rootName)) {
473                pathIndex++;
474                break;
475            }
476        }
477        if (pathIndex >= path.length) {
478            throw new OMAException("Bad node-path: " + pathString);
479        }
480        LinkedList<String> tailPath = new LinkedList<>();
481        while (pathIndex < path.length) {
482            tailPath.add(path[pathIndex]);
483            pathIndex++;
484        }
485        return tailPath;
486    }
487
488    public HomeSP getHomeSP(String fqdn) {
489        return mSPs.get(fqdn);
490    }
491
492    public void removeSP(String fqdn) throws IOException {
493        if (mSPs.remove(fqdn) == null) {
494            Log.d(OSUManager.TAG, "No HS20 profile to delete for " + fqdn);
495            return;
496        }
497
498        Log.d(OSUManager.TAG, "Deleting HS20 profile for " + fqdn);
499
500        MOTree moTree;
501        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
502            moTree = MOTree.unmarshal(in);
503            OMAConstructed tbd = findTargetTree(moTree, fqdn);
504            if (tbd == null) {
505                throw new IOException("Node " + fqdn + " doesn't exist in MO tree");
506            }
507            OMAConstructed pps = moTree.getRoot();
508            OMANode removed = pps.removeNode("?", tbd);
509            if (removed == null) {
510                throw new IOException("Failed to remove " + fqdn + " out of MO tree");
511            }
512        }
513        writeMO(moTree, mPpsFile);
514    }
515
516    public MOTree getMOTree(HomeSP homeSP) throws IOException {
517        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(mPpsFile))) {
518            MOTree moTree = MOTree.unmarshal(in);
519            OMAConstructed target = findTargetTree(moTree, homeSP.getFQDN());
520            if (target == null) {
521                throw new IOException("Can't find " + homeSP.getFQDN() + " in MO tree");
522            }
523            return MOTree.buildMgmtTree(OMAConstants.PPS_URN, OMAConstants.OMAVersion, target);
524        }
525    }
526
527    private static void writeMO(MOTree moTree, File f) throws IOException {
528        try (BufferedOutputStream out =
529                     new BufferedOutputStream(new FileOutputStream(f, false))) {
530            moTree.marshal(out);
531            out.flush();
532        }
533    }
534
535    private static String fqdnList(Collection<HomeSP> sps) {
536        StringBuilder sb = new StringBuilder();
537        boolean first = true;
538        for (HomeSP sp : sps) {
539            if (first) {
540                first = false;
541            } else {
542                sb.append(", ");
543            }
544            sb.append(sp.getFQDN());
545        }
546        return sb.toString();
547    }
548
549    private static OMANode buildHomeSPTree(HomeSP homeSP, OMAConstructed root, int instanceID)
550            throws IOException {
551        OMANode providerSubNode = root.addChild(getInstanceString(instanceID),
552                null, null, null);
553
554        // The HomeSP:
555        OMANode homeSpNode = providerSubNode.addChild(TAG_HomeSP, null, null, null);
556        if (!homeSP.getSSIDs().isEmpty()) {
557            OMAConstructed nwkIDNode =
558                    (OMAConstructed) homeSpNode.addChild(TAG_NetworkID, null, null, null);
559            int instance = 0;
560            for (Map.Entry<String, Long> entry : homeSP.getSSIDs().entrySet()) {
561                OMAConstructed inode =
562                        (OMAConstructed) nwkIDNode
563                                .addChild(getInstanceString(instance++), null, null, null);
564                inode.addChild(TAG_SSID, null, entry.getKey(), null);
565                if (entry.getValue() != null) {
566                    inode.addChild(TAG_HESSID, null,
567                            String.format("%012x", entry.getValue()), null);
568                }
569            }
570        }
571
572        homeSpNode.addChild(TAG_FriendlyName, null, homeSP.getFriendlyName(), null);
573
574        if (homeSP.getIconURL() != null) {
575            homeSpNode.addChild(TAG_IconURL, null, homeSP.getIconURL(), null);
576        }
577
578        homeSpNode.addChild(TAG_FQDN, null, homeSP.getFQDN(), null);
579
580        if (!homeSP.getMatchAllOIs().isEmpty() || !homeSP.getMatchAnyOIs().isEmpty()) {
581            OMAConstructed homeOIList =
582                    (OMAConstructed) homeSpNode.addChild(TAG_HomeOIList, null, null, null);
583
584            int instance = 0;
585            for (Long oi : homeSP.getMatchAllOIs()) {
586                OMAConstructed inode =
587                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
588                                null, null, null);
589                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
590                inode.addChild(TAG_HomeOIRequired, null, "TRUE", null);
591            }
592            for (Long oi : homeSP.getMatchAnyOIs()) {
593                OMAConstructed inode =
594                        (OMAConstructed) homeOIList.addChild(getInstanceString(instance++),
595                                null, null, null);
596                inode.addChild(TAG_HomeOI, null, String.format("%x", oi), null);
597                inode.addChild(TAG_HomeOIRequired, null, "FALSE", null);
598            }
599        }
600
601        if (!homeSP.getOtherHomePartners().isEmpty()) {
602            OMAConstructed otherPartners =
603                    (OMAConstructed) homeSpNode.addChild(TAG_OtherHomePartners, null, null, null);
604            int instance = 0;
605            for (String fqdn : homeSP.getOtherHomePartners()) {
606                OMAConstructed inode =
607                        (OMAConstructed) otherPartners.addChild(getInstanceString(instance++),
608                                null, null, null);
609                inode.addChild(TAG_FQDN, null, fqdn, null);
610            }
611        }
612
613        if (!homeSP.getRoamingConsortiums().isEmpty()) {
614            homeSpNode.addChild(TAG_RoamingConsortiumOI, null,
615                    getRCList(homeSP.getRoamingConsortiums()), null);
616        }
617
618        // The Credential:
619        OMANode credentialNode = providerSubNode.addChild(TAG_Credential, null, null, null);
620        Credential cred = homeSP.getCredential();
621        EAPMethod method = cred.getEAPMethod();
622
623        if (cred.getCtime() > 0) {
624            credentialNode.addChild(TAG_CreationDate,
625                    null, DTFormat.format(new Date(cred.getCtime())), null);
626        }
627        if (cred.getExpTime() > 0) {
628            credentialNode.addChild(TAG_ExpirationDate,
629                    null, DTFormat.format(new Date(cred.getExpTime())), null);
630        }
631
632        if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_SIM
633                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKA
634                || method.getEAPMethodID() == EAP.EAPMethodID.EAP_AKAPrim) {
635
636            OMANode simNode = credentialNode.addChild(TAG_SIM, null, null, null);
637            simNode.addChild(TAG_IMSI, null, cred.getImsi().toString(), null);
638            simNode.addChild(TAG_EAPType, null,
639                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
640
641        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TTLS) {
642
643            OMANode unpNode = credentialNode.addChild(TAG_UsernamePassword, null, null, null);
644            unpNode.addChild(TAG_Username, null, cred.getUserName(), null);
645            unpNode.addChild(TAG_Password, null,
646                    Base64.encodeToString(cred.getPassword().getBytes(StandardCharsets.UTF_8),
647                            Base64.DEFAULT), null);
648            OMANode eapNode = unpNode.addChild(TAG_EAPMethod, null, null, null);
649            eapNode.addChild(TAG_EAPType, null,
650                    Integer.toString(EAP.mapEAPMethod(method.getEAPMethodID())), null);
651            eapNode.addChild(TAG_InnerMethod, null,
652                    ((NonEAPInnerAuth) method.getAuthParam()).getOMAtype(), null);
653
654        } else if (method.getEAPMethodID() == EAP.EAPMethodID.EAP_TLS) {
655
656            OMANode certNode = credentialNode.addChild(TAG_DigitalCertificate, null, null, null);
657            certNode.addChild(TAG_CertificateType, null, Credential.CertTypeX509, null);
658            certNode.addChild(TAG_CertSHA256Fingerprint, null,
659                    Utils.toHex(cred.getFingerPrint()), null);
660
661        } else {
662            throw new OMAException("Invalid credential on " + homeSP.getFQDN());
663        }
664
665        credentialNode.addChild(TAG_Realm, null, cred.getRealm(), null);
666
667        // !!! Note: This node defines CRL checking through OSCP, I suspect we won't be able
668        // to do that so it is commented out:
669        //credentialNode.addChild(TAG_CheckAAAServerCertStatus, null, "TRUE", null);
670        return providerSubNode;
671    }
672
673    private static String getInstanceString(int instance) {
674        return "r1i" + instance;
675    }
676
677    private static String getRCList(Collection<Long> rcs) {
678        StringBuilder builder = new StringBuilder();
679        boolean first = true;
680        for (Long roamingConsortium : rcs) {
681            if (first) {
682                first = false;
683            } else {
684                builder.append(',');
685            }
686            builder.append(String.format("%x", roamingConsortium));
687        }
688        return builder.toString();
689    }
690
691    public static List<HomeSP> buildSPs(MOTree moTree) throws OMAException {
692        OMAConstructed spList;
693        List<HomeSP> homeSPs = new ArrayList<>();
694        if (moTree.getRoot().getName().equals(TAG_PerProviderSubscription)) {
695            // The old PPS file was rooted at PPS instead of MgmtTree to conserve space
696            spList = moTree.getRoot();
697
698            if (spList == null) {
699                return homeSPs;
700            }
701
702            for (OMANode node : spList.getChildren()) {
703                if (!node.isLeaf()) {
704                    homeSPs.add(buildHomeSP(node, 0));
705                }
706            }
707        } else {
708            for (OMANode ppsRoot : moTree.getRoot().getChildren()) {
709                if (ppsRoot.getName().equals(TAG_PerProviderSubscription)) {
710                    Integer updateIdentifier = null;
711                    OMANode instance = null;
712                    for (OMANode child : ppsRoot.getChildren()) {
713                        if (child.getName().equals(TAG_UpdateIdentifier)) {
714                            updateIdentifier = getInteger(child);
715                        } else if (!child.isLeaf()) {
716                            instance = child;
717                        }
718                    }
719                    if (instance == null) {
720                        throw new OMAException("PPS node missing instance node");
721                    }
722                    homeSPs.add(buildHomeSP(instance,
723                            updateIdentifier != null ? updateIdentifier : 0));
724                }
725            }
726        }
727
728        return homeSPs;
729    }
730
731    private static HomeSP buildHomeSP(OMANode ppsRoot, int updateIdentifier) throws OMAException {
732        OMANode spRoot = ppsRoot.getChild(TAG_HomeSP);
733
734        String fqdn = spRoot.getScalarValue(Arrays.asList(TAG_FQDN).iterator());
735        String friendlyName = spRoot.getScalarValue(Arrays.asList(TAG_FriendlyName).iterator());
736        String iconURL = spRoot.getScalarValue(Arrays.asList(TAG_IconURL).iterator());
737
738        HashSet<Long> roamingConsortiums = new HashSet<>();
739        String oiString = spRoot.getScalarValue(Arrays.asList(TAG_RoamingConsortiumOI).iterator());
740        if (oiString != null) {
741            for (String oi : oiString.split(",")) {
742                roamingConsortiums.add(Long.parseLong(oi.trim(), 16));
743            }
744        }
745
746        Map<String, Long> ssids = new HashMap<>();
747
748        OMANode ssidListNode = spRoot.getListValue(Arrays.asList(TAG_NetworkID).iterator());
749        if (ssidListNode != null) {
750            for (OMANode ssidRoot : ssidListNode.getChildren()) {
751                OMANode hessidNode = ssidRoot.getChild(TAG_HESSID);
752                ssids.put(ssidRoot.getChild(TAG_SSID).getValue(), getMac(hessidNode));
753            }
754        }
755
756        Set<Long> matchAnyOIs = new HashSet<>();
757        List<Long> matchAllOIs = new ArrayList<>();
758        OMANode homeOIListNode = spRoot.getListValue(Arrays.asList(TAG_HomeOIList).iterator());
759        if (homeOIListNode != null) {
760            for (OMANode homeOIRoot : homeOIListNode.getChildren()) {
761                String homeOI = homeOIRoot.getChild(TAG_HomeOI).getValue();
762                if (Boolean.parseBoolean(homeOIRoot.getChild(TAG_HomeOIRequired).getValue())) {
763                    matchAllOIs.add(Long.parseLong(homeOI, 16));
764                } else {
765                    matchAnyOIs.add(Long.parseLong(homeOI, 16));
766                }
767            }
768        }
769
770        Set<String> otherHomePartners = new HashSet<>();
771        OMANode otherListNode =
772                spRoot.getListValue(Arrays.asList(TAG_OtherHomePartners).iterator());
773        if (otherListNode != null) {
774            for (OMANode fqdnNode : otherListNode.getChildren()) {
775                otherHomePartners.add(fqdnNode.getChild(TAG_FQDN).getValue());
776            }
777        }
778
779        Credential credential = buildCredential(ppsRoot.getChild(TAG_Credential));
780
781        OMANode policyNode = ppsRoot.getChild(TAG_Policy);
782        Policy policy = policyNode != null ? new Policy(policyNode) : null;
783
784        Map<String, String> aaaTrustRoots;
785        OMANode aaaRootNode = ppsRoot.getChild(TAG_AAAServerTrustRoot);
786        if (aaaRootNode == null) {
787            aaaTrustRoots = null;
788        } else {
789            aaaTrustRoots = new HashMap<>(aaaRootNode.getChildren().size());
790            for (OMANode child : aaaRootNode.getChildren()) {
791                aaaTrustRoots.put(getString(child, TAG_CertURL),
792                        getString(child, TAG_CertSHA256Fingerprint));
793            }
794        }
795
796        OMANode updateNode = ppsRoot.getChild(TAG_SubscriptionUpdate);
797        UpdateInfo subscriptionUpdate = updateNode != null ? new UpdateInfo(updateNode) : null;
798        OMANode subNode = ppsRoot.getChild(TAG_SubscriptionParameters);
799        SubscriptionParameters subscriptionParameters = subNode != null ?
800                new SubscriptionParameters(subNode) : null;
801
802        return new HomeSP(ssids, fqdn, roamingConsortiums, otherHomePartners,
803                matchAnyOIs, matchAllOIs, friendlyName, iconURL, credential,
804                policy, getInteger(ppsRoot.getChild(TAG_CredentialPriority), 0),
805                aaaTrustRoots, subscriptionUpdate, subscriptionParameters, updateIdentifier);
806    }
807
808    private static Credential buildCredential(OMANode credNode) throws OMAException {
809        long ctime = getTime(credNode.getChild(TAG_CreationDate));
810        long expTime = getTime(credNode.getChild(TAG_ExpirationDate));
811        String realm = getString(credNode.getChild(TAG_Realm));
812        boolean checkAAACert = getBoolean(credNode.getChild(TAG_CheckAAAServerCertStatus));
813
814        OMANode unNode = credNode.getChild(TAG_UsernamePassword);
815        OMANode certNode = credNode.getChild(TAG_DigitalCertificate);
816        OMANode simNode = credNode.getChild(TAG_SIM);
817
818        int alternatives = 0;
819        alternatives += unNode != null ? 1 : 0;
820        alternatives += certNode != null ? 1 : 0;
821        alternatives += simNode != null ? 1 : 0;
822        if (alternatives != 1) {
823            throw new OMAException("Expected exactly one credential type, got " + alternatives);
824        }
825
826        if (unNode != null) {
827            String userName = getString(unNode.getChild(TAG_Username));
828            String password = getString(unNode.getChild(TAG_Password));
829            boolean machineManaged = getBoolean(unNode.getChild(TAG_MachineManaged));
830            String softTokenApp = getString(unNode.getChild(TAG_SoftTokenApp));
831            boolean ableToShare = getBoolean(unNode.getChild(TAG_AbleToShare));
832
833            OMANode eapMethodNode = unNode.getChild(TAG_EAPMethod);
834            int eapID = getInteger(eapMethodNode.getChild(TAG_EAPType));
835
836            EAP.EAPMethodID eapMethodID = EAP.mapEAPMethod(eapID);
837            if (eapMethodID == null) {
838                throw new OMAException("Unknown EAP method: " + eapID);
839            }
840
841            Long vid = getOptionalInteger(eapMethodNode.getChild(TAG_VendorId));
842            Long vtype = getOptionalInteger(eapMethodNode.getChild(TAG_VendorType));
843            Long innerEAPType = getOptionalInteger(eapMethodNode.getChild(TAG_InnerEAPType));
844            EAP.EAPMethodID innerEAPMethod = null;
845            if (innerEAPType != null) {
846                innerEAPMethod = EAP.mapEAPMethod(innerEAPType.intValue());
847                if (innerEAPMethod == null) {
848                    throw new OMAException("Bad inner EAP method: " + innerEAPType);
849                }
850            }
851
852            Long innerVid = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorID));
853            Long innerVtype = getOptionalInteger(eapMethodNode.getChild(TAG_InnerVendorType));
854            String innerNonEAPMethod = getString(eapMethodNode.getChild(TAG_InnerMethod));
855
856            EAPMethod eapMethod;
857            if (innerEAPMethod != null) {
858                eapMethod = new EAPMethod(eapMethodID, new InnerAuthEAP(innerEAPMethod));
859            } else if (vid != null) {
860                eapMethod = new EAPMethod(eapMethodID,
861                        new ExpandedEAPMethod(EAP.AuthInfoID.ExpandedEAPMethod,
862                                vid.intValue(), vtype));
863            } else if (innerVid != null) {
864                eapMethod =
865                        new EAPMethod(eapMethodID, new ExpandedEAPMethod(EAP.AuthInfoID
866                                .ExpandedInnerEAPMethod, innerVid.intValue(), innerVtype));
867            } else if (innerNonEAPMethod != null) {
868                eapMethod = new EAPMethod(eapMethodID, new NonEAPInnerAuth(innerNonEAPMethod));
869            } else {
870                throw new OMAException("Incomplete set of EAP parameters");
871            }
872
873            return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, userName,
874                    password, machineManaged, softTokenApp, ableToShare);
875        }
876        if (certNode != null) {
877            try {
878                String certTypeString = getString(certNode.getChild(TAG_CertificateType));
879                byte[] fingerPrint = getOctets(certNode.getChild(TAG_CertSHA256Fingerprint));
880
881                EAPMethod eapMethod = new EAPMethod(EAP.EAPMethodID.EAP_TLS, null);
882
883                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod,
884                        Credential.mapCertType(certTypeString), fingerPrint);
885            } catch (NumberFormatException nfe) {
886                throw new OMAException("Bad hex string: " + nfe.toString());
887            }
888        }
889        if (simNode != null) {
890            try {
891                IMSIParameter imsi = new IMSIParameter(getString(simNode.getChild(TAG_IMSI)));
892
893                EAPMethod eapMethod =
894                        new EAPMethod(EAP.mapEAPMethod(getInteger(simNode.getChild(TAG_EAPType))),
895                                null);
896
897                return new Credential(ctime, expTime, realm, checkAAACert, eapMethod, imsi);
898            } catch (IOException ioe) {
899                throw new OMAException("Failed to parse IMSI: " + ioe);
900            }
901        }
902        throw new OMAException("Missing credential parameters");
903    }
904
905    public static OMANode getChild(OMANode node, String key) throws OMAException {
906        OMANode child = node.getChild(key);
907        if (child == null) {
908            throw new OMAException("No such node: " + key);
909        }
910        return child;
911    }
912
913    public static String getString(OMANode node, String key) throws OMAException {
914        OMANode child = node.getChild(key);
915        if (child == null) {
916            throw new OMAException("Missing value for " + key);
917        } else if (!child.isLeaf()) {
918            throw new OMAException(key + " is not a leaf node");
919        }
920        return child.getValue();
921    }
922
923    public static long getLong(OMANode node, String key, Long dflt) throws OMAException {
924        OMANode child = node.getChild(key);
925        if (child == null) {
926            if (dflt != null) {
927                return dflt;
928            } else {
929                throw new OMAException("Missing value for " + key);
930            }
931        } else {
932            if (!child.isLeaf()) {
933                throw new OMAException(key + " is not a leaf node");
934            }
935            String value = child.getValue();
936            try {
937                long result = Long.parseLong(value);
938                if (result < 0) {
939                    throw new OMAException("Negative value for " + key);
940                }
941                return result;
942            } catch (NumberFormatException nfe) {
943                throw new OMAException("Value for " + key + " is non-numeric: " + value);
944            }
945        }
946    }
947
948    public static <T> T getSelection(OMANode node, String key) throws OMAException {
949        OMANode child = node.getChild(key);
950        if (child == null) {
951            throw new OMAException("Missing value for " + key);
952        } else if (!child.isLeaf()) {
953            throw new OMAException(key + " is not a leaf node");
954        }
955        return getSelection(key, child.getValue());
956    }
957
958    public static <T> T getSelection(String key, String value) throws OMAException {
959        if (value == null) {
960            throw new OMAException("No value for " + key);
961        }
962        Map<String, Object> kvp = sSelectionMap.get(key);
963        T result = (T) kvp.get(value.toLowerCase());
964        if (result == null) {
965            throw new OMAException("Invalid value '" + value + "' for " + key);
966        }
967        return result;
968    }
969
970    private static boolean getBoolean(OMANode boolNode) {
971        return boolNode != null && Boolean.parseBoolean(boolNode.getValue());
972    }
973
974    public static String getString(OMANode stringNode) {
975        return stringNode != null ? stringNode.getValue() : null;
976    }
977
978    private static int getInteger(OMANode intNode, int dflt) throws OMAException {
979        if (intNode == null) {
980            return dflt;
981        }
982        return getInteger(intNode);
983    }
984
985    private static int getInteger(OMANode intNode) throws OMAException {
986        if (intNode == null) {
987            throw new OMAException("Missing integer value");
988        }
989        try {
990            return Integer.parseInt(intNode.getValue());
991        } catch (NumberFormatException nfe) {
992            throw new OMAException("Invalid integer: " + intNode.getValue());
993        }
994    }
995
996    private static Long getMac(OMANode macNode) throws OMAException {
997        if (macNode == null) {
998            return null;
999        }
1000        try {
1001            return Long.parseLong(macNode.getValue(), 16);
1002        } catch (NumberFormatException nfe) {
1003            throw new OMAException("Invalid MAC: " + macNode.getValue());
1004        }
1005    }
1006
1007    private static Long getOptionalInteger(OMANode intNode) throws OMAException {
1008        if (intNode == null) {
1009            return null;
1010        }
1011        try {
1012            return Long.parseLong(intNode.getValue());
1013        } catch (NumberFormatException nfe) {
1014            throw new OMAException("Invalid integer: " + intNode.getValue());
1015        }
1016    }
1017
1018    public static long getTime(OMANode timeNode) throws OMAException {
1019        if (timeNode == null) {
1020            return Utils.UNSET_TIME;
1021        }
1022        String timeText = timeNode.getValue();
1023        try {
1024            Date date = DTFormat.parse(timeText);
1025            return date.getTime();
1026        } catch (ParseException pe) {
1027            throw new OMAException("Badly formatted time: " + timeText);
1028        }
1029    }
1030
1031    private static byte[] getOctets(OMANode octetNode) throws OMAException {
1032        if (octetNode == null) {
1033            throw new OMAException("Missing byte value");
1034        }
1035        return Utils.hexToBytes(octetNode.getValue());
1036    }
1037}
1038