diff options
author | Felipe Leme <felipeal@google.com> | 2017-09-21 10:13:07 -0700 |
---|---|---|
committer | Felipe Leme <felipeal@google.com> | 2017-09-25 13:14:04 -0700 |
commit | 500b70f82bafaf5d39bb3eb98ee680b90f380798 (patch) | |
tree | ee949b452c94c9c3490faf156f8ada99d10f2305 /input/autofill | |
parent | 4547cb7002a1c1086752409ba1f2708f7d00ad0e (diff) | |
download | android-500b70f82bafaf5d39bb3eb98ee680b90f380798.tar.gz |
Initial Digital Asset Links support (DAL).
This change uses DAL to verify that an app being autofilled is linked to
the web domain set in a node, and fail if the association could not be verified.
It also includes some minor improvements, such as:
- Clears cache on WebView activity.
- Takes an optional url extra in the WebView activity intent.
- Uses FillCallback.onFailure() to report some errors.
Bug: 66414472
Bug: 66417779
Bug: 66900717
Test: manual verification
Change-Id: I2fa96ae61201d7d11aaab168957da2a5685a5764
Diffstat (limited to 'input/autofill')
13 files changed, 376 insertions, 65 deletions
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml index a5abfe86..0af57162 100644 --- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml +++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ android:versionCode="1" android:versionName="1.0"> + <uses-permission android:name="android.permission.INTERNET"/> + <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java index 84b5a976..8c429d01 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java @@ -31,6 +31,7 @@ public final class CommonUtil { public static final String TAG = "AutofillSample"; public static final boolean DEBUG = true; + public static final boolean VERBOSE = false; public static final String EXTRA_DATASET_NAME = "dataset_name"; public static final String EXTRA_FOR_RESPONSE = "for_response"; diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java index 9ef84280..616529a2 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java @@ -19,9 +19,14 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import static com.example.android.autofillframework.CommonUtil.DEBUG; +import static com.example.android.autofillframework.CommonUtil.TAG; + import com.example.android.autofillframework.R; public class WebViewSignInActivity extends AppCompatActivity { @@ -38,7 +43,19 @@ public class WebViewSignInActivity extends AppCompatActivity { setContentView(R.layout.login_webview_activity); WebView webView = findViewById(R.id.webview); + WebSettings webSettings = webView.getSettings(); webView.setWebViewClient(new WebViewClient()); - webView.loadUrl("file:///android_res/raw/sample_form.html"); + webSettings.setJavaScriptEnabled(true); + + String url = getIntent().getStringExtra("url"); + if (url == null) { + url = "file:///android_res/raw/sample_form.html"; + } + if (DEBUG) Log.d(TAG, "Clearing WebView data"); + webView.clearHistory(); + webView.clearFormData(); + webView.clearCache(true); + Log.i(TAG, "Loading URL " + url); + webView.loadUrl(url); } }
\ No newline at end of file diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java index 015dc68f..631cc0a9 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java @@ -124,7 +124,7 @@ public class AuthActivity extends AppCompatActivity { Intent intent = getIntent(); boolean forResponse = intent.getBooleanExtra(EXTRA_FOR_RESPONSE, true); AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE); - StructureParser parser = new StructureParser(structure); + StructureParser parser = new StructureParser(getApplicationContext(), structure); parser.parseForFill(); AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields(); int saveTypes = autofillFields.getSaveType(); diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java index 0b47cfd1..4c0f173e 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java @@ -38,6 +38,10 @@ import static com.example.android.autofillframework.CommonUtil.TAG; */ public final class AutofillHelper { + private AutofillHelper() { + throw new UnsupportedOperationException("provide static methods only"); + } + /** * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the * client View. diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java index f30e91b5..ac030221 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java @@ -29,7 +29,6 @@ import android.service.autofill.SaveRequest; import android.util.Log; import android.view.autofill.AutofillId; import android.widget.RemoteViews; -import android.widget.Toast; import com.example.android.autofillframework.R; import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsAutofillRepository; @@ -41,8 +40,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; -import static com.example.android.autofillframework.CommonUtil.DEBUG; import static com.example.android.autofillframework.CommonUtil.TAG; +import static com.example.android.autofillframework.CommonUtil.VERBOSE; import static com.example.android.autofillframework.CommonUtil.bundleToString; import static com.example.android.autofillframework.CommonUtil.dumpStructure; @@ -56,13 +55,13 @@ public class MyAutofillService extends AutofillService { String packageName = structure.getActivityComponent().getPackageName(); if (!SharedPrefsPackageVerificationRepository.getInstance() .putPackageSignatures(getApplicationContext(), packageName)) { - Toast.makeText(getApplicationContext(), R.string.invalid_package_signature, - Toast.LENGTH_SHORT).show(); + callback.onFailure( + getApplicationContext().getString(R.string.invalid_package_signature)); return; } final Bundle data = request.getClientState(); - if (DEBUG) { - Log.d(TAG, "onFillRequest(): data=" + bundleToString(data)); + if (VERBOSE) { + Log.v(TAG, "onFillRequest(): data=" + bundleToString(data)); dumpStructure(structure); } @@ -73,8 +72,17 @@ public class MyAutofillService extends AutofillService { } }); // Parse AutoFill data in Activity - StructureParser parser = new StructureParser(structure); - parser.parseForFill(); + StructureParser parser = new StructureParser(getApplicationContext(), structure); + // TODO: try / catch on other places (onSave, auth activity, etc...) + try { + parser.parseForFill(); + } catch (SecurityException e) { + // TODO: handle cases where DAL didn't pass by showing a custom UI asking the user + // to confirm the mapping. Might require subclassing SecurityException. + Log.w(TAG, "Security exception handling " + request, e); + callback.onFailure(e.getMessage()); + return; + } AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields(); FillResponse.Builder responseBuilder = new FillResponse.Builder(); // Check user's settings for authenticating Responses and Datasets. @@ -108,16 +116,16 @@ public class MyAutofillService extends AutofillService { String packageName = structure.getActivityComponent().getPackageName(); if (!SharedPrefsPackageVerificationRepository.getInstance() .putPackageSignatures(getApplicationContext(), packageName)) { - Toast.makeText(getApplicationContext(), R.string.invalid_package_signature, - Toast.LENGTH_SHORT).show(); + callback.onFailure( + getApplicationContext().getString(R.string.invalid_package_signature)); return; } final Bundle data = request.getClientState(); - if (DEBUG) { - Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data)); + if (VERBOSE) { + Log.v(TAG, "onSaveRequest(): data=" + bundleToString(data)); dumpStructure(structure); } - StructureParser parser = new StructureParser(structure); + StructureParser parser = new StructureParser(getApplicationContext(), structure); parser.parseForSave(); FilledAutofillFieldCollection filledAutofillFieldCollection = parser.getClientFormData(); SharedPrefsAutofillRepository.getInstance() diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java new file mode 100644 index 00000000..3d13b6d3 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static com.example.android.autofillframework.CommonUtil.DEBUG; +import static com.example.android.autofillframework.CommonUtil.TAG; +import static com.example.android.autofillframework.CommonUtil.VERBOSE; + +/** + * Helper class for security checks. + */ +public final class SecurityHelper { + + private static final String REST_TEMPLATE = + "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?" + + "source.web.site=%s&relation=delegate_permission/%s" + + "&target.android_app.package_name=%s" + + "&target.android_app.certificate.sha256_fingerprint=%s"; + + private static final String PERMISSION_GET_LOGIN_CREDS = "common.get_login_creds"; + private static final String PERMISSION_HANDLE_ALL_URLS = "common.handle_all_urls"; + + private SecurityHelper() { + throw new UnsupportedOperationException("provides static methods only"); + } + + private static boolean isValidSync(String webDomain, String permission, String packageName, + String fingerprint) { + if (DEBUG) Log.d(TAG, "validating domain " + webDomain + " for pkg " + packageName + + " and fingerprint " + fingerprint + " for permission" + permission); + if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:") ) { + // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's + // assume it's https + webDomain = "https://" + webDomain; + } + + String restUrl = + String.format(REST_TEMPLATE, webDomain, permission, packageName, fingerprint); + if (DEBUG) Log.d(TAG, "DAL REST request: " + restUrl); + + HttpURLConnection urlConnection = null; + StringBuilder output = new StringBuilder(); + try { + URL url = new URL(restUrl); + urlConnection = (HttpURLConnection) url.openConnection(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(urlConnection.getInputStream()))) { + String line = null; + while ((line = reader.readLine()) != null) { + output.append(line); + } + } + String response = output.toString(); + if (VERBOSE) Log.v(TAG, "DAL REST Response: " + response); + + JSONObject jsonObject = new JSONObject(response); + boolean valid = jsonObject.optBoolean("linked", false); + if (DEBUG) Log.d(TAG, "Valid: " + valid); + + return valid; + } catch (Exception e) { + throw new RuntimeException("Failed to validate", e); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + + } + + private static boolean isValidSync(String webDomain, String packageName, String fingerprint) { + boolean isValid = + isValidSync(webDomain, PERMISSION_GET_LOGIN_CREDS, packageName, fingerprint); + if (!isValid) { + // Ideally we should only check for the get_login_creds, but not all domains set + // it yet, so validating for handle_all_urls gives a higher coverage. + if (DEBUG) { + Log.d(TAG, PERMISSION_GET_LOGIN_CREDS + " validation failed; trying " + + PERMISSION_HANDLE_ALL_URLS); + } + isValid = isValidSync(webDomain, PERMISSION_HANDLE_ALL_URLS, packageName, fingerprint); + } + return isValid; + } + + + public static boolean isValid(String webDomain, String packageName, String fingerprint) { + if (DEBUG) Log.d(TAG, "validating domain " + webDomain + " for pkg " + packageName + + " and fingerprint " + fingerprint ); + final String fullDomain; + if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:") ) { + // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's + // assume it's https + fullDomain = "https://" + webDomain; + } else { + fullDomain = webDomain; + } + + // TODO: use the DAL Java API or a better REST alternative like Volley + // and/or document it should not block until it returns (for example, the server could + // start parsing the structure while it waits for the result. + AsyncTask<String, Integer, Boolean> task = new AsyncTask<String, Integer, Boolean>() { + @Override + protected Boolean doInBackground(String... strings) { + return isValidSync(fullDomain, packageName, fingerprint); + } + }; + try { + return task.execute((String[]) null).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.w(TAG, "Thread interrupted"); + } catch (Exception e) { + Log.w(TAG, "Async task failed", e); + } + return false; + } + + /** + * Gets the fingerprint of the signed certificate of a package. + */ + public static String getFingerprint(Context context, String packageName) throws Exception { + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + Signature[] signatures = packageInfo.signatures; + if (signatures.length != 1) { + throw new SecurityException(packageName + " has " + signatures.length + " signatures"); + } + byte[] cert = signatures[0].toByteArray(); + try (InputStream input = new ByteArrayInputStream(cert)) { + CertificateFactory factory = CertificateFactory.getInstance("X509"); + X509Certificate x509 = (X509Certificate) factory.generateCertificate(input); + MessageDigest md = MessageDigest.getInstance("SHA256"); + byte[] publicKey = md.digest(x509.getEncoded()); + return toHexFormat(publicKey); + } + } + + private static String toHexFormat(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(bytes[i]); + int length = hex.length(); + if (length == 1) { + hex = "0" + hex; + } + if (length > 2) { + hex = hex.substring(length - 2, length); + } + builder.append(hex.toUpperCase()); + if (i < (bytes.length - 1)) { + builder.append(':'); + } + } + return builder.toString(); + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java index 812ba40f..a912c0fa 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java @@ -18,13 +18,17 @@ package com.example.android.autofillframework.multidatasetservice; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; +import android.content.Context; import android.util.Log; import android.view.autofill.AutofillValue; +import com.example.android.autofillframework.R; +import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsDigitalAssetLinksRepository; import com.example.android.autofillframework.multidatasetservice.model.FilledAutofillField; import com.example.android.autofillframework.multidatasetservice.model.FilledAutofillFieldCollection; import static com.example.android.autofillframework.CommonUtil.TAG; +import static com.example.android.autofillframework.CommonUtil.DEBUG; /** * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an @@ -34,10 +38,12 @@ import static com.example.android.autofillframework.CommonUtil.TAG; final class StructureParser { private final AutofillFieldMetadataCollection mAutofillFields = new AutofillFieldMetadataCollection(); + private final Context mContext; private final AssistStructure mStructure; private FilledAutofillFieldCollection mFilledAutofillFieldCollection; - StructureParser(AssistStructure structure) { + StructureParser(Context context, AssistStructure structure) { + mContext = context; mStructure = structure; } @@ -53,17 +59,42 @@ final class StructureParser { * Traverse AssistStructure and add ViewNode metadata to a flat list. */ private void parse(boolean forFill) { - Log.d(TAG, "Parsing structure for " + mStructure.getActivityComponent()); + if (DEBUG) Log.d(TAG, "Parsing structure for " + mStructure.getActivityComponent()); int nodes = mStructure.getWindowNodeCount(); mFilledAutofillFieldCollection = new FilledAutofillFieldCollection(); + StringBuilder webDomain = new StringBuilder(); for (int i = 0; i < nodes; i++) { WindowNode node = mStructure.getWindowNodeAt(i); ViewNode view = node.getRootViewNode(); - parseLocked(forFill, view); + parseLocked(forFill, view, webDomain); + } + if (webDomain.length() > 0 ) { + String packageName = mStructure.getActivityComponent().getPackageName(); + boolean valid = SharedPrefsDigitalAssetLinksRepository.getInstance().isValid(mContext, + webDomain.toString(), packageName); + if (!valid) { + throw new SecurityException(mContext.getString( + R.string.invalid_link_association, webDomain, packageName)); + } + if (DEBUG) Log.d(TAG, "Domain " + webDomain + " is valid for " + packageName); + } else { + if (DEBUG) Log.d(TAG, "no web domain"); } } - private void parseLocked(boolean forFill, ViewNode viewNode) { + private void parseLocked(boolean forFill, ViewNode viewNode, StringBuilder validWebDomain) { + String webDomain = viewNode.getWebDomain(); + if (webDomain != null) { + if (DEBUG) Log.d(TAG, "child web domain: " + webDomain); + if (validWebDomain.length() > 0) { + if (!webDomain.equals(validWebDomain.toString())) { + throw new SecurityException("Found multiple web domains: valid= " + + validWebDomain + ", child=" + webDomain); + } + } else { + validWebDomain.append(webDomain); + } + } if (viewNode.getAutofillHints() != null) { String[] filteredHints = AutofillHints.filterForSupportedHints( @@ -92,7 +123,7 @@ final class StructureParser { int childrenSize = viewNode.getChildCount(); if (childrenSize > 0) { for (int i = 0; i < childrenSize; i++) { - parseLocked(forFill, viewNode.getChildAt(i)); + parseLocked(forFill, viewNode.getChildAt(i), validWebDomain); } } } diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java new file mode 100644 index 00000000..04624cbb --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice.datasource; + +import android.content.Context; + +/** + * Helper format + * <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a> needs. + */ +public interface DigitalAssetLinksDataSource { + + /** + * Checks if the association between a web domain and a package is valid. + */ + boolean isValid(Context context, String webDomain, String packageName); + + /** + * Clears all cached data. + */ + void clear(Context context); +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java index 7b55ef26..91a1923e 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java @@ -28,8 +28,9 @@ import java.util.Set; /** * Singleton autofill data repository that stores autofill fields to SharedPreferences. - * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done - * here only for simplicity and learning purposes. + * + * <p><b>Disclaimer</b>: you should not store sensitive fields like user data unencrypted. + * This is done here only for simplicity and learning purposes. */ public class SharedPrefsAutofillRepository implements AutofillDataSource { private static final String SHARED_PREF_KEY = "com.example.android.autofillframework" diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java new file mode 100644 index 00000000..fdfd6578 --- /dev/null +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice.datasource; + +import android.content.Context; +import android.util.Log; + +import com.example.android.autofillframework.multidatasetservice.SecurityHelper; + +import static com.example.android.autofillframework.CommonUtil.TAG; + +/** + * Singleton repository that caches the result of Digital Asset Links checks. + */ +public class SharedPrefsDigitalAssetLinksRepository implements DigitalAssetLinksDataSource { + + private static SharedPrefsDigitalAssetLinksRepository sInstance; + + private SharedPrefsDigitalAssetLinksRepository() { + } + + public static SharedPrefsDigitalAssetLinksRepository getInstance() { + if (sInstance == null) { + sInstance = new SharedPrefsDigitalAssetLinksRepository(); + } + return sInstance; + } + + @Override + public boolean isValid(Context context, String webDomain, String packageName) { + // TODO: implement caching. It could cache the whole domain -> (packagename, fingerprint), + // but then either invalidate when the package change or when the DAL association times out + // (the maxAge is part of the API response), or document that a real-life service + // should do that. + + String fingerprint = null; + try { + fingerprint = SecurityHelper.getFingerprint(context, packageName); + } catch (Exception e) { + Log.w(TAG, "error getting fingerprint for " + packageName, e); + return false; + } + return SecurityHelper.isValid(webDomain,packageName,fingerprint); + } + + @Override + public void clear(Context context) { + // TODO: implement once if caches results or remove from the interface + } +} diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java index b7bb5828..aa467786 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java @@ -17,16 +17,9 @@ package com.example.android.autofillframework.multidatasetservice.datasource; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; import android.util.Log; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; +import com.example.android.autofillframework.multidatasetservice.SecurityHelper; import static com.example.android.autofillframework.CommonUtil.TAG; @@ -58,7 +51,7 @@ public class SharedPrefsPackageVerificationRepository implements PackageVerifica public boolean putPackageSignatures(Context context, String packageName) { String hash; try { - hash = getCertificateHash(context, packageName); + hash = SecurityHelper.getFingerprint(context, packageName); Log.d(TAG, "Hash for " + packageName + ": " + hash); } catch (Exception e) { Log.w(TAG, "Error getting hash for " + packageName + ": " + e); @@ -89,38 +82,4 @@ public class SharedPrefsPackageVerificationRepository implements PackageVerifica SHARED_PREF_KEY, Context.MODE_PRIVATE); return hash.equals(prefs.getString(packageName, null)); } - - private String getCertificateHash(Context context, String packageName) - throws Exception { - PackageManager pm = context.getPackageManager(); - PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); - Signature[] signatures = packageInfo.signatures; - byte[] cert = signatures[0].toByteArray(); - try (InputStream input = new ByteArrayInputStream(cert)) { - CertificateFactory factory = CertificateFactory.getInstance("X509"); - X509Certificate x509 = (X509Certificate) factory.generateCertificate(input); - MessageDigest md = MessageDigest.getInstance("SHA256"); - byte[] publicKey = md.digest(x509.getEncoded()); - return toHexFormat(publicKey); - } - } - - private String toHexFormat(byte[] bytes) { - StringBuilder builder = new StringBuilder(bytes.length * 2); - for (int i = 0; i < bytes.length; i++) { - String hex = Integer.toHexString(bytes[i]); - int length = hex.length(); - if (length == 1) { - hex = "0" + hex; - } - if (length > 2) { - hex = hex.substring(length - 2, length); - } - builder.append(hex.toUpperCase()); - if (i < (bytes.length - 1)) { - builder.append(':'); - } - } - return builder.toString(); - } } diff --git a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml index 229cf9e8..01a743b5 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ <string name="cc_exp_month_description">Credit Card Expiration Month</string> <string name="cc_exp_year_description">Credit Card Expiration Year</string> <string name="invalid_package_signature">Invalid package signature</string> + <string name="invalid_link_association">Could not associate web domain %1$s with app %2$s</string> <string name="edittext_login_info">This is a sample login page that uses standard EditTexts from the UI toolkit. EditTexts are already optimized for autofill so extra autofill-specific code is almost never needed. |