• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 org.json.JSONArray;
20 import org.json.JSONException;
21 import org.json.JSONObject;
22 
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import 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 
getCertFingerprints()56     public List<String> getCertFingerprints() {
57         return Collections.unmodifiableList(mCertFingerprints);
58     }
59 
getPackageName()60     public String getPackageName() {
61         return mPackageName;
62     }
63 
64     @Override
toJson()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
toString()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
equals(Object o)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
hashCode()93     public int hashCode() {
94         return toJson().hashCode();
95     }
96 
97     @Override
lookupKey()98     public int lookupKey() {
99         return getPackageName().hashCode();
100     }
101 
102     @Override
followInsecureInclude()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      */
create(JSONObject asset)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      */
create(String packageName, List<String> certFingerprints)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 
AndroidAppAsset(String packageName, List<String> certFingerprints)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      */
sortAndDeDuplicate(List<String> certs)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