Home | History | Annotate | Download | only in multidatasetservice
      1 /*
      2  * Copyright (C) 2017 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 package com.example.android.autofillframework.multidatasetservice;
     17 
     18 import android.content.Context;
     19 import android.content.pm.PackageInfo;
     20 import android.content.pm.PackageManager;
     21 import android.content.pm.Signature;
     22 import android.os.AsyncTask;
     23 import android.util.Log;
     24 
     25 import com.google.common.net.InternetDomainName;
     26 
     27 import org.json.JSONObject;
     28 
     29 import java.io.BufferedReader;
     30 import java.io.ByteArrayInputStream;
     31 import java.io.InputStream;
     32 import java.io.InputStreamReader;
     33 import java.net.HttpURLConnection;
     34 import java.net.URL;
     35 import java.security.MessageDigest;
     36 import java.security.cert.CertificateFactory;
     37 import java.security.cert.X509Certificate;
     38 
     39 import static com.example.android.autofillframework.CommonUtil.DEBUG;
     40 import static com.example.android.autofillframework.CommonUtil.TAG;
     41 import static com.example.android.autofillframework.CommonUtil.VERBOSE;
     42 
     43 /**
     44  * Helper class for security checks.
     45  */
     46 public final class SecurityHelper {
     47 
     48     private static final String REST_TEMPLATE =
     49             "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?"
     50                     + "source.web.site=%s&relation=delegate_permission/%s"
     51                     + "&target.android_app.package_name=%s"
     52                     + "&target.android_app.certificate.sha256_fingerprint=%s";
     53 
     54     private static final String PERMISSION_GET_LOGIN_CREDS = "common.get_login_creds";
     55     private static final String PERMISSION_HANDLE_ALL_URLS = "common.handle_all_urls";
     56 
     57     private SecurityHelper() {
     58         throw new UnsupportedOperationException("provides static methods only");
     59     }
     60 
     61     private static boolean isValidSync(String webDomain, String permission, String packageName,
     62             String fingerprint) {
     63         if (DEBUG) Log.d(TAG, "validating domain " + webDomain + " for pkg " + packageName
     64                 + " and fingerprint " + fingerprint + " for permission" + permission);
     65         if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:")) {
     66             // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's
     67             // assume it's https
     68             webDomain = "https://" + webDomain;
     69         }
     70 
     71         String restUrl =
     72                 String.format(REST_TEMPLATE, webDomain, permission, packageName, fingerprint);
     73         if (DEBUG) Log.d(TAG, "DAL REST request: " + restUrl);
     74 
     75         HttpURLConnection urlConnection = null;
     76         StringBuilder output = new StringBuilder();
     77         try {
     78             URL url = new URL(restUrl);
     79             urlConnection = (HttpURLConnection) url.openConnection();
     80             try (BufferedReader reader = new BufferedReader(
     81                     new InputStreamReader(urlConnection.getInputStream()))) {
     82                 String line = null;
     83                 while ((line = reader.readLine()) != null) {
     84                     output.append(line);
     85                 }
     86             }
     87             String response = output.toString();
     88             if (VERBOSE) Log.v(TAG, "DAL REST Response: " + response);
     89 
     90             JSONObject jsonObject = new JSONObject(response);
     91             boolean valid = jsonObject.optBoolean("linked", false);
     92             if (DEBUG) Log.d(TAG, "Valid: " + valid);
     93 
     94             return valid;
     95         } catch (Exception e) {
     96             throw new RuntimeException("Failed to validate", e);
     97         } finally {
     98             if (urlConnection != null) {
     99                 urlConnection.disconnect();
    100             }
    101         }
    102 
    103     }
    104 
    105     private static boolean isValidSync(String webDomain, String packageName, String fingerprint) {
    106         boolean isValid =
    107                 isValidSync(webDomain, PERMISSION_GET_LOGIN_CREDS, packageName, fingerprint);
    108         if (!isValid) {
    109             // Ideally we should only check for the get_login_creds, but not all domains set
    110             // it yet, so validating for handle_all_urls gives a higher coverage.
    111             if (DEBUG) {
    112                 Log.d(TAG, PERMISSION_GET_LOGIN_CREDS + " validation failed; trying "
    113                         + PERMISSION_HANDLE_ALL_URLS);
    114             }
    115             isValid = isValidSync(webDomain, PERMISSION_HANDLE_ALL_URLS, packageName, fingerprint);
    116         }
    117         return isValid;
    118     }
    119 
    120     public static String getCanonicalDomain(String domain) {
    121         InternetDomainName idn = InternetDomainName.from(domain);
    122         while (idn != null && !idn.isTopPrivateDomain()) {
    123             idn = idn.parent();
    124         }
    125         return idn == null ? null : idn.toString();
    126     }
    127 
    128     public static boolean isValid(String webDomain, String packageName, String fingerprint) {
    129         String canonicalDomain = getCanonicalDomain(webDomain);
    130         if (DEBUG) Log.d(TAG, "validating domain " + canonicalDomain + " (" + webDomain
    131                 + ") for pkg " + packageName + " and fingerprint " + fingerprint);
    132         final String fullDomain;
    133         if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:")) {
    134             // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's
    135             // assume it's https
    136             fullDomain = "https://" + canonicalDomain;
    137         } else {
    138             fullDomain = canonicalDomain;
    139         }
    140 
    141         // TODO: use the DAL Java API or a better REST alternative like Volley
    142         // and/or document it should not block until it returns (for example, the server could
    143         // start parsing the structure while it waits for the result.
    144         AsyncTask<String, Integer, Boolean> task = new AsyncTask<String, Integer, Boolean>() {
    145             @Override
    146             protected Boolean doInBackground(String... strings) {
    147                 return isValidSync(fullDomain, packageName, fingerprint);
    148             }
    149         };
    150         try {
    151             return task.execute((String[]) null).get();
    152         } catch (InterruptedException e) {
    153             Thread.currentThread().interrupt();
    154             Log.w(TAG, "Thread interrupted");
    155         } catch (Exception e) {
    156             Log.w(TAG, "Async task failed", e);
    157         }
    158         return false;
    159     }
    160 
    161     /**
    162      * Gets the fingerprint of the signed certificate of a package.
    163      */
    164     public static String getFingerprint(Context context, String packageName) throws Exception {
    165         PackageManager pm = context.getPackageManager();
    166         PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
    167         Signature[] signatures = packageInfo.signatures;
    168         if (signatures.length != 1) {
    169             throw new SecurityException(packageName + " has " + signatures.length + " signatures");
    170         }
    171         byte[] cert = signatures[0].toByteArray();
    172         try (InputStream input = new ByteArrayInputStream(cert)) {
    173             CertificateFactory factory = CertificateFactory.getInstance("X509");
    174             X509Certificate x509 = (X509Certificate) factory.generateCertificate(input);
    175             MessageDigest md = MessageDigest.getInstance("SHA256");
    176             byte[] publicKey = md.digest(x509.getEncoded());
    177             return toHexFormat(publicKey);
    178         }
    179     }
    180 
    181     private static String toHexFormat(byte[] bytes) {
    182         StringBuilder builder = new StringBuilder(bytes.length * 2);
    183         for (int i = 0; i < bytes.length; i++) {
    184             String hex = Integer.toHexString(bytes[i]);
    185             int length = hex.length();
    186             if (length == 1) {
    187                 hex = "0" + hex;
    188             }
    189             if (length > 2) {
    190                 hex = hex.substring(length - 2, length);
    191             }
    192             builder.append(hex.toUpperCase());
    193             if (i < (bytes.length - 1)) {
    194                 builder.append(':');
    195             }
    196         }
    197         return builder.toString();
    198     }
    199 }
    200