| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.statementservice.retriever; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * Immutable value type that names an Android app asset. |
| * |
| * <p>An Android app can be named by its package name and certificate fingerprints using this JSON |
| * string: { "namespace": "android_app", "package_name": "[Java package name]", |
| * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] } |
| * |
| * <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp", |
| * "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"] |
| * } |
| * |
| * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: |
| * {@code keytool -list -printcert -jarfile signed_app.apk} |
| * |
| * <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...) |
| * representing the certificate SHA-256 fingerprint. |
| */ |
| /* package private */ final class AndroidAppAsset extends AbstractAsset { |
| |
| private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set."; |
| private static final String MISSING_APPCERTS_FORMAT_STRING = |
| "Expected %s to be non-empty array."; |
| private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings."; |
| |
| private final List<String> mCertFingerprints; |
| private final String mPackageName; |
| |
| public List<String> getCertFingerprints() { |
| return Collections.unmodifiableList(mCertFingerprints); |
| } |
| |
| public String getPackageName() { |
| return mPackageName; |
| } |
| |
| @Override |
| public String toJson() { |
| AssetJsonWriter writer = new AssetJsonWriter(); |
| |
| writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP); |
| writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName); |
| writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints); |
| |
| return writer.closeAndGetString(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder asset = new StringBuilder(); |
| asset.append("AndroidAppAsset: "); |
| asset.append(toJson()); |
| return asset.toString(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof AndroidAppAsset)) { |
| return false; |
| } |
| |
| return ((AndroidAppAsset) o).toJson().equals(toJson()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return toJson().hashCode(); |
| } |
| |
| @Override |
| public int lookupKey() { |
| return getPackageName().hashCode(); |
| } |
| |
| @Override |
| public boolean followInsecureInclude() { |
| // Non-HTTPS includes are not allowed in Android App assets. |
| return false; |
| } |
| |
| /** |
| * Checks that the input is a valid Android app asset. |
| * |
| * @param asset a JSONObject that has "namespace", "package_name", and |
| * "sha256_cert_fingerprints" fields. |
| * @throws AssociationServiceException if the asset is not well formatted. |
| */ |
| public static AndroidAppAsset create(JSONObject asset) |
| throws AssociationServiceException { |
| String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME); |
| if (packageName.equals("")) { |
| throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING, |
| Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME)); |
| } |
| |
| JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS); |
| if (certArray == null || certArray.length() == 0) { |
| throw new AssociationServiceException( |
| String.format(MISSING_APPCERTS_FORMAT_STRING, |
| Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); |
| } |
| List<String> certFingerprints = new ArrayList<>(certArray.length()); |
| for (int i = 0; i < certArray.length(); i++) { |
| try { |
| certFingerprints.add(certArray.getString(i)); |
| } catch (JSONException e) { |
| throw new AssociationServiceException( |
| String.format(APPCERT_NOT_STRING_FORMAT_STRING, |
| Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); |
| } |
| } |
| |
| return new AndroidAppAsset(packageName, certFingerprints); |
| } |
| |
| /** |
| * Creates a new AndroidAppAsset. |
| * |
| * @param packageName the package name of the Android app. |
| * @param certFingerprints at least one of the Android app signing certificate sha-256 |
| * fingerprint. |
| */ |
| public static AndroidAppAsset create(String packageName, List<String> certFingerprints) { |
| if (packageName == null || packageName.equals("")) { |
| throw new AssertionError("Expected packageName to be set."); |
| } |
| if (certFingerprints == null || certFingerprints.size() == 0) { |
| throw new AssertionError("Expected certFingerprints to be set."); |
| } |
| List<String> lowerFps = new ArrayList<String>(certFingerprints.size()); |
| for (String fp : certFingerprints) { |
| lowerFps.add(fp.toUpperCase(Locale.US)); |
| } |
| return new AndroidAppAsset(packageName, lowerFps); |
| } |
| |
| private AndroidAppAsset(String packageName, List<String> certFingerprints) { |
| if (packageName.equals("")) { |
| mPackageName = null; |
| } else { |
| mPackageName = packageName; |
| } |
| |
| if (certFingerprints == null || certFingerprints.size() == 0) { |
| mCertFingerprints = null; |
| } else { |
| mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints)); |
| } |
| } |
| |
| /** |
| * Returns an ASCII-sorted copy of the list of certs with all duplicates removed. |
| */ |
| private List<String> sortAndDeDuplicate(List<String> certs) { |
| if (certs.size() <= 1) { |
| return certs; |
| } |
| HashSet<String> set = new HashSet<>(certs); |
| List<String> result = new ArrayList<>(set); |
| Collections.sort(result); |
| return result; |
| } |
| |
| } |