Home | History | Annotate | Download | only in retriever
      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