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