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