1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.statementservice.retriever;
18
19import org.json.JSONArray;
20import org.json.JSONException;
21import org.json.JSONObject;
22
23import java.util.ArrayList;
24import java.util.Collections;
25import java.util.HashSet;
26import java.util.List;
27import java.util.Locale;
28
29/**
30 * Immutable value type that names an Android app asset.
31 *
32 * <p>An Android app can be named by its package name and certificate fingerprints using this JSON
33 * string: { "namespace": "android_app", "package_name": "[Java package name]",
34 * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
35 *
36 * <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp",
37 * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"]
38 * }
39 *
40 * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
41 * {@code keytool -list -printcert -jarfile signed_app.apk}
42 *
43 * <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
44 * representing the certificate SHA-256 fingerprint.
45 */
46/* package private */ final class AndroidAppAsset extends AbstractAsset {
47
48    private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
49    private static final String MISSING_APPCERTS_FORMAT_STRING =
50            "Expected %s to be non-empty array.";
51    private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings.";
52
53    private final List<String> mCertFingerprints;
54    private final String mPackageName;
55
56    public List<String> getCertFingerprints() {
57        return Collections.unmodifiableList(mCertFingerprints);
58    }
59
60    public String getPackageName() {
61        return mPackageName;
62    }
63
64    @Override
65    public String toJson() {
66        AssetJsonWriter writer = new AssetJsonWriter();
67
68        writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
69        writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
70        writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
71
72        return writer.closeAndGetString();
73    }
74
75    @Override
76    public String toString() {
77        StringBuilder asset = new StringBuilder();
78        asset.append("AndroidAppAsset: ");
79        asset.append(toJson());
80        return asset.toString();
81    }
82
83    @Override
84    public boolean equals(Object o) {
85        if (!(o instanceof AndroidAppAsset)) {
86            return false;
87        }
88
89        return ((AndroidAppAsset) o).toJson().equals(toJson());
90    }
91
92    @Override
93    public int hashCode() {
94        return toJson().hashCode();
95    }
96
97    @Override
98    public int lookupKey() {
99        return getPackageName().hashCode();
100    }
101
102    @Override
103    public boolean followInsecureInclude() {
104        // Non-HTTPS includes are not allowed in Android App assets.
105        return false;
106    }
107
108    /**
109     * Checks that the input is a valid Android app asset.
110     *
111     * @param asset a JSONObject that has "namespace", "package_name", and
112     *              "sha256_cert_fingerprints" fields.
113     * @throws AssociationServiceException if the asset is not well formatted.
114     */
115    public static AndroidAppAsset create(JSONObject asset)
116            throws AssociationServiceException {
117        String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
118        if (packageName.equals("")) {
119            throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
120                    Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
121        }
122
123        JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
124        if (certArray == null || certArray.length() == 0) {
125            throw new AssociationServiceException(
126                    String.format(MISSING_APPCERTS_FORMAT_STRING,
127                            Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
128        }
129        List<String> certFingerprints = new ArrayList<>(certArray.length());
130        for (int i = 0; i < certArray.length(); i++) {
131            try {
132                certFingerprints.add(certArray.getString(i));
133            } catch (JSONException e) {
134                throw new AssociationServiceException(
135                        String.format(APPCERT_NOT_STRING_FORMAT_STRING,
136                                Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
137            }
138        }
139
140        return new AndroidAppAsset(packageName, certFingerprints);
141    }
142
143    /**
144     * Creates a new AndroidAppAsset.
145     *
146     * @param packageName the package name of the Android app.
147     * @param certFingerprints at least one of the Android app signing certificate sha-256
148     *                         fingerprint.
149     */
150    public static AndroidAppAsset create(String packageName, List<String> certFingerprints) {
151        if (packageName == null || packageName.equals("")) {
152            throw new AssertionError("Expected packageName to be set.");
153        }
154        if (certFingerprints == null || certFingerprints.size() == 0) {
155            throw new AssertionError("Expected certFingerprints to be set.");
156        }
157        List<String> lowerFps = new ArrayList<String>(certFingerprints.size());
158        for (String fp : certFingerprints) {
159            lowerFps.add(fp.toUpperCase(Locale.US));
160        }
161        return new AndroidAppAsset(packageName, lowerFps);
162    }
163
164    private AndroidAppAsset(String packageName, List<String> certFingerprints) {
165        if (packageName.equals("")) {
166            mPackageName = null;
167        } else {
168            mPackageName = packageName;
169        }
170
171        if (certFingerprints == null || certFingerprints.size() == 0) {
172            mCertFingerprints = null;
173        } else {
174            mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints));
175        }
176    }
177
178    /**
179     * Returns an ASCII-sorted copy of the list of certs with all duplicates removed.
180     */
181    private List<String> sortAndDeDuplicate(List<String> certs) {
182        if (certs.size() <= 1) {
183            return certs;
184        }
185        HashSet<String> set = new HashSet<>(certs);
186        List<String> result = new ArrayList<>(set);
187        Collections.sort(result);
188        return result;
189    }
190
191}
192