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