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 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