Merge remote-tracking branch 'goog/stage-aosp-master' into HEAD
am: be06f9efe5
Change-Id: I207bd3b4148621fde1ea345da7f92a4d3527c3f6
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index ae51f98..841f1bd 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -22,7 +22,7 @@
<string name="requesting_application" msgid="1589142627467598421">"অ্যাপ্লিকেশান %s একটি সার্টিফিকেটের অনুরোধ করেছে। একটি সার্টিফিকেট চয়ন করলে তা অ্যাপ্লিকেশানটিকে এখন এবং ভবিষ্যতে সার্ভারগুলির সঙ্গে এই পরিচয় ব্যবহার করতে দেবে।"</string>
<string name="requesting_server" msgid="5832565605998634370">"অ্যাপ্লিকেশানটি অনুরোধ করা সার্ভারকে %s হিসেবে শনাক্ত করেছে, আপনার কাছে অ্যাপ্লিকেশানটি বিশ্বস্ত হলে তবেই আপনি অ্যাপ্লিকেশানটিকে সার্টিফিকেট অ্যাক্সেস দিতে পারবেন।"</string>
<string name="install_new_cert_message" msgid="4451971501142085495">"বহিরাগত সঞ্চয়স্থানে অবস্থিত %1$s বা %2$s এক্সটেনশান সহ PKCS#12 ফাইল থেকে আপনি আপনার সার্টিফিকেটগুলি ইনস্টল করতে পারেন।"</string>
- <string name="install_new_cert_button_label" msgid="903474285774077171">"শংসাপত্র ইনস্টল করুন"</string>
+ <string name="install_new_cert_button_label" msgid="903474285774077171">"সার্টিফিকেট ইনস্টল করুন"</string>
<string name="allow_button" msgid="3030990695030371561">"বেছে নিন"</string>
<string name="deny_button" msgid="3766539809121892584">"আস্বীকার করুন"</string>
</resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..36b09a7
--- /dev/null
+++ b/res/values-en-rCA/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2011 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="170210454004696382">"Key Chain"</string>
+ <string name="title_no_certs" msgid="8350009443064722873">"No certificates found"</string>
+ <string name="title_select_cert" msgid="3588447616418041699">"Choose certificate"</string>
+ <string name="requesting_application" msgid="1589142627467598421">"The %s app has requested a certificate. Choosing a certificate will let the app use this identity with servers now and in the future."</string>
+ <string name="requesting_server" msgid="5832565605998634370">"The app has identified the requesting server as %s, but you should only give the app access to the certificate if you trust the app."</string>
+ <string name="install_new_cert_message" msgid="4451971501142085495">"You can install certificates from a PKCS#12 file with a %1$s or a %2$s extension located in external storage."</string>
+ <string name="install_new_cert_button_label" msgid="903474285774077171">"Install certificate"</string>
+ <string name="allow_button" msgid="3030990695030371561">"Select"</string>
+ <string name="deny_button" msgid="3766539809121892584">"Deny"</string>
+</resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..b75e2e9
--- /dev/null
+++ b/res/values-en-rXC/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2011 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="170210454004696382">"Key Chain"</string>
+ <string name="title_no_certs" msgid="8350009443064722873">"No certificates found"</string>
+ <string name="title_select_cert" msgid="3588447616418041699">"Choose certificate"</string>
+ <string name="requesting_application" msgid="1589142627467598421">"The app %s has requested a certificate. Choosing a certificate will let the app use this identity with servers now and in the future."</string>
+ <string name="requesting_server" msgid="5832565605998634370">"The app has identified the requesting server as %s, but you should only give the app access to the certificate if you trust the app."</string>
+ <string name="install_new_cert_message" msgid="4451971501142085495">"You can install certificates from a PKCS#12 file with a %1$s or a %2$s extension located in external storage."</string>
+ <string name="install_new_cert_button_label" msgid="903474285774077171">"Install certificate"</string>
+ <string name="allow_button" msgid="3030990695030371561">"Select"</string>
+ <string name="deny_button" msgid="3766539809121892584">"Deny"</string>
+</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index b61cbe7..d7bfde4 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -19,8 +19,8 @@
<string name="app_name" msgid="170210454004696382">"की चेन"</string>
<string name="title_no_certs" msgid="8350009443064722873">"कोई प्रमाणपत्र नहीं मिला"</string>
<string name="title_select_cert" msgid="3588447616418041699">"प्रमाणपत्र चुनें"</string>
- <string name="requesting_application" msgid="1589142627467598421">"%s ऐप्स ने प्रमाणपत्र का अनुरोध किया है. प्रमाणपत्र चुनने से ऐप्स इस पहचान का उपयोग अभी और भविष्य में सर्वर के साथ कर सकेगा."</string>
- <string name="requesting_server" msgid="5832565605998634370">"ऐप्स ने अनुरोध करने वाले सर्वर को %s के रूप में पहचाना है, लेकिन आपको ऐप्स को प्रमाणपत्र पर केवल तब ही पहुंच देना चाहिए जब आप ऐप्स पर विश्वास करते हों."</string>
+ <string name="requesting_application" msgid="1589142627467598421">"%s ऐप ने प्रमाणपत्र का अनुरोध किया है. प्रमाणपत्र चुनने से ऐप सर्वर के साथ इस पहचान का इस्तेमाल अभी और आने वाले समय में कर सकेगा."</string>
+ <string name="requesting_server" msgid="5832565605998634370">"ऐप ने अनुरोध करने वाले सर्वर को %s के तौर पर पहचाना है, लेकिन आपको ऐप को प्रमाणपत्र पर केवल तब ही पहुंच देनी चाहिए, जब आप ऐप पर भरोसा करते हों."</string>
<string name="install_new_cert_message" msgid="4451971501142085495">"आप बाहरी मेमोरी में मौजूद %1$s या %2$s एक्सटेंशन वाली PKCS#12 फ़ाइल से प्रमाणपत्र इंस्टॉल कर सकते हैं."</string>
<string name="install_new_cert_button_label" msgid="903474285774077171">"प्रमाणपत्र इंस्टॉल करें"</string>
<string name="allow_button" msgid="3030990695030371561">"चुनें"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index e0eb18b..42aad08 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -22,7 +22,7 @@
<string name="requesting_application" msgid="1589142627467598421">"%s ಅಪ್ಲಿಕೇಶನ್ ಪ್ರಮಾಣಪತ್ರವೊಂದನ್ನು ವಿನಂತಿಸಿದೆ. ಪ್ರಮಾಣಪತ್ರವನ್ನು ಆಯ್ಕೆ ಮಾಡುವುದರಿಂದ ಸರ್ವರ್ಗಳೊಂದಿಗೆ ಈಗ ಮತ್ತು ಭವಿಷ್ಯದಲ್ಲಿ ಈ ಗುರುತನ್ನು ಬಳಸಲು ಈ ಅಪ್ಲಿಕೇಶನ್ಗೆ ಅನುಮತಿಸುತ್ತದೆ."</string>
<string name="requesting_server" msgid="5832565605998634370">"ಅಪ್ಲಿಕೇಶನ್ ವಿನಂತಿಸಿದ ಸರ್ವರ್ ಅನ್ನು %s ರಂತೆ ಗುರುತಿಸಿದೆ, ಆದರೆ ನೀವು ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ನಂಬಿದರೆ ಮಾತ್ರ ಪ್ರಮಾಣಪತ್ರಕ್ಕೆ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರವೇಶವನ್ನು ನೀಡಬೇಕು."</string>
<string name="install_new_cert_message" msgid="4451971501142085495">"ಬಾಹ್ಯ ಸಂಗ್ರಹಣೆಯಲ್ಲಿ ಸಂಗ್ರಹವಾಗಿರುವ %1$s ಅಥವಾ %2$s ವಿಸ್ತರಣೆಯೊಂದಿಗೆ PKCS#12 ಫೈಲ್ನಿಂದ ನೀವು ಪ್ರಮಾಣಪತ್ರಗಳನ್ನು ಸ್ಥಾಪಿಸಬಹುದು."</string>
- <string name="install_new_cert_button_label" msgid="903474285774077171">"ಪ್ರಮಾಣಪತ್ರವನ್ನು ಸ್ಥಾಪಿಸು"</string>
+ <string name="install_new_cert_button_label" msgid="903474285774077171">"ಪ್ರಮಾಣಪತ್ರವನ್ನು ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ"</string>
<string name="allow_button" msgid="3030990695030371561">"ಆಯ್ಕೆಮಾಡಿ"</string>
<string name="deny_button" msgid="3766539809121892584">"ನಿರಾಕರಿಸಿ"</string>
</resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 27de709..ac91bef 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -20,9 +20,9 @@
<string name="title_no_certs" msgid="8350009443064722873">"कोणतीही प्रमाणपत्रे आढळली नाहीत"</string>
<string name="title_select_cert" msgid="3588447616418041699">"प्रमाणपत्र निवडा"</string>
<string name="requesting_application" msgid="1589142627467598421">"%s अॅप एका प्रमाणपत्राची विनंती करत आहे. एक प्रमाणपत्र निवडणे या अॅपला या सर्व्हरसह आता आणि भविष्यात ही ओळख वापरू देईल."</string>
- <string name="requesting_server" msgid="5832565605998634370">"अॅपने विनंती केलेल्या सर्व्हरला %s म्हणून ओळखले आहे, परंतु आपला अॅपवर विश्वास असल्यास आपण अॅपला केवळ प्रमाणपत्रामध्ये प्रवेश द्यावा."</string>
- <string name="install_new_cert_message" msgid="4451971501142085495">"आपण अतिरिक्त संचयनामध्ये स्थित %1$s किंवा %2$s विस्तारासह एका PKCS#12 फाईलवरुन प्रमाणपत्र स्थापित करु शकता."</string>
- <string name="install_new_cert_button_label" msgid="903474285774077171">"प्रमाणपत्र स्थापित करा"</string>
+ <string name="requesting_server" msgid="5832565605998634370">"अॅपने विनंती केलेल्या सर्व्हरला %s म्हणून ओळखले आहे, परंतु आपला अॅपवर विश्वास असेल तरच आपण अॅपला प्रमाणपत्रामध्ये प्रवेश द्यावा."</string>
+ <string name="install_new_cert_message" msgid="4451971501142085495">"आपण अतिरिक्त स्टोरेजमध्ये स्थित %1$s किंवा %2$s विस्तारासह एका PKCS#12 फाइलवरुन प्रमाणपत्र इंस्टॉल करु शकता."</string>
+ <string name="install_new_cert_button_label" msgid="903474285774077171">"प्रमाणपत्र इंस्टॉल करा"</string>
<string name="allow_button" msgid="3030990695030371561">"निवडा"</string>
<string name="deny_button" msgid="3766539809121892584">"नकार द्या"</string>
</resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index ca2309f..3b26efd 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -18,10 +18,10 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" msgid="170210454004696382">"ਕੁੰਜੀ ਚੇਨ"</string>
<string name="title_no_certs" msgid="8350009443064722873">"ਕੋਈ ਸਰਟੀਫਿਕੇਟ ਨਹੀਂ ਮਿਲੇ"</string>
- <string name="title_select_cert" msgid="3588447616418041699">"ਸਰਟੀਫਿਕੇਟ ਚੁਣੋ"</string>
- <string name="requesting_application" msgid="1589142627467598421">"ਐਪ %s ਨੇ ਇੱਕ ਸਰਟੀਫਿਕੇਟ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ। ਇੱਕ ਸਰਟੀਫਿਕੇਟ ਚੁਣਨ ਨਾਲ ਇਹ ਐਪ ਨੂੰ ਹੁਣ ਅਤੇ ਭਵਿੱਖ ਵਿੱਚ ਸਰਵਰਾਂ ਨਾਲ ਇਹ ਪਛਾਣ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।"</string>
- <string name="requesting_server" msgid="5832565605998634370">"ਐਪ ਨੇ ਬੇਨਤੀ ਕਰਨ ਵਾਲੇ ਸਰਵਰ ਦੀ ਪਛਾਣ %s ਦੇ ਤੌਰ ਤੇ ਕੀਤੀ ਹੈ, ਪਰੰਤੂ ਤੁਹਾਨੂੰ ਐਪ ਨੂੰ ਸਰਟੀਫਿਕੇਟ ਤੱਕ ਕੇਵਲ ਤਾਂ ਹੀ ਪਹੁੰਚ ਦੇਣੀ ਚਾਹੀਦੀ ਹੈ ਜੇਕਰ ਤੁਸੀਂ ਐਪ ਤੇ ਭਰੋਸਾ ਕਰਦੇ ਹੋ।"</string>
- <string name="install_new_cert_message" msgid="4451971501142085495">"ਤੁਸੀਂ ਬਾਹਰੀ ਸਟੋਰੇਜ ਵਿੱਚ ਸਥਿਤ ਇੱਕ %1$s ਜਾਂ ਇੱਕ %2$s ਐਕਸਟੈਂਸ਼ਨ ਨਾਲ ਇੱਕ PKCS#12 ਫਾਈਲ ਵਿੱਚੋਂ ਸਰਟੀਫਿਕੇਟ ਇੰਸੌਟਲ ਕਰ ਸਕਦੇ ਹੋ।"</string>
+ <string name="title_select_cert" msgid="3588447616418041699">"ਪ੍ਰਮਾਣ-ਪੱਤਰ ਚੁਣੋ"</string>
+ <string name="requesting_application" msgid="1589142627467598421">"ਐਪ %s ਨੇ ਇੱਕ ਪ੍ਰਮਾਣ-ਪੱਤਰ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ। ਇੱਕ ਪ੍ਰਮਾਣ-ਪੱਤਰ ਚੁਣਨ ਨਾਲ ਇਹ ਐਪ ਨੂੰ ਹੁਣ ਅਤੇ ਭਵਿੱਖ ਵਿੱਚ ਸਰਵਰਾਂ ਨਾਲ ਇਹ ਪਛਾਣ ਵਰਤਣ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।"</string>
+ <string name="requesting_server" msgid="5832565605998634370">"ਐਪ ਨੇ ਬੇਨਤੀ ਕਰਨ ਵਾਲੇ ਸਰਵਰ ਦੀ ਪਛਾਣ %s ਦੇ ਤੌਰ ਤੇ ਕੀਤੀ ਹੈ, ਪਰੰਤੂ ਤੁਹਾਨੂੰ ਐਪ ਨੂੰ ਸਰਟੀਫਿਕੇਟ ਤੱਕ ਸਿਰਫ਼ ਤਾਂ ਹੀ ਪਹੁੰਚ ਦੇਣੀ ਚਾਹੀਦੀ ਹੈ ਜੇਕਰ ਤੁਸੀਂ ਐਪ ਤੇ ਭਰੋਸਾ ਕਰਦੇ ਹੋ।"</string>
+ <string name="install_new_cert_message" msgid="4451971501142085495">"ਤੁਸੀਂ ਬਾਹਰੀ ਸਟੋਰੇਜ ਵਿੱਚ ਸਥਿਤ ਇੱਕ %1$s ਜਾਂ ਇੱਕ %2$s ਐਕਸਟੈਂਸ਼ਨ ਨਾਲ ਇੱਕ PKCS#12 ਫ਼ਾਈਲ ਵਿੱਚੋਂ ਪ੍ਰਮਾਣ-ਪੱਤਰ ਸਥਾਪਤ ਕਰ ਸਕਦੇ ਹੋ।"</string>
<string name="install_new_cert_button_label" msgid="903474285774077171">"ਪ੍ਰਮਾਣ-ਪੱਤਰ ਸਥਾਪਤ ਕਰੋ"</string>
<string name="allow_button" msgid="3030990695030371561">"ਚੁਣੋ"</string>
<string name="deny_button" msgid="3766539809121892584">"ਅਸਵੀਕਾਰ ਕਰੋ"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index aadaa04..02b9b51 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -19,8 +19,8 @@
<string name="app_name" msgid="170210454004696382">"కీ చెయిన్"</string>
<string name="title_no_certs" msgid="8350009443064722873">"ప్రమాణపత్రాలు కనుగొనబడలేదు"</string>
<string name="title_select_cert" msgid="3588447616418041699">"ప్రమాణపత్రాన్ని ఎంచుకోండి"</string>
- <string name="requesting_application" msgid="1589142627467598421">"అనువర్తనం %s ప్రమాణపత్రాన్ని అభ్యర్థించింది. ప్రమాణపత్రాన్ని ఎంచుకోవడం వలన ఇప్పుడు మరియు భవిష్యత్తులో సర్వర్లతో ఈ గుర్తింపును ఉపయోగించడానికి అనువర్తనం అనుమతించబడుతుంది."</string>
- <string name="requesting_server" msgid="5832565605998634370">"అనువర్తనం అభ్యర్థిస్తున్న సర్వర్ను %sగా గుర్తించింది, కానీ మీరు అనువర్తనాన్ని విశ్వసిస్తే మాత్రమే ప్రమాణపత్రం కోసం అనువర్తనానికి ప్రాప్యతను అందించాలి."</string>
+ <string name="requesting_application" msgid="1589142627467598421">"యాప్ %s ప్రమాణపత్రాన్ని అభ్యర్థించింది. ప్రమాణపత్రాన్ని ఎంచుకోవడం వలన ఇప్పుడు మరియు భవిష్యత్తులో సర్వర్లతో ఈ గుర్తింపును ఉపయోగించడానికి యాప్ అనుమతించబడుతుంది."</string>
+ <string name="requesting_server" msgid="5832565605998634370">"యాప్ అభ్యర్థిస్తున్న సర్వర్ను %sగా గుర్తించింది, కానీ మీరు యాప్ను విశ్వసిస్తే మాత్రమే సర్టిఫికెట్ కోసం యాప్నకు యాక్సెస్ను అందించాలి."</string>
<string name="install_new_cert_message" msgid="4451971501142085495">"మీరు బాహ్య నిల్వలో ఉండే %1$s లేదా %2$s పొడిగింపుతో PKCS#12 ఫైల్ నుండి ప్రమాణపత్రాలను ఇన్స్టాల్ చేయవచ్చు."</string>
<string name="install_new_cert_button_label" msgid="903474285774077171">"ప్రమాణపత్రాన్ని ఇన్స్టాల్ చేయి"</string>
<string name="allow_button" msgid="3030990695030371561">"ఎంచుకోండి"</string>
diff --git a/robotests/Android.mk b/robotests/Android.mk
new file mode 100644
index 0000000..e0a4f86
--- /dev/null
+++ b/robotests/Android.mk
@@ -0,0 +1,45 @@
+#############################################
+# KeyChain Robolectric test target. #
+#############################################
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Include the testing libraries (JUnit4 + Robolectric libs).
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ mockito-robolectric-prebuilt \
+ platform-robolectric-android-all-stubs \
+ truth-prebuilt
+
+LOCAL_JAVA_LIBRARIES := \
+ junit \
+ platform-robolectric-3.4.2-prebuilt \
+ telephony-common
+
+LOCAL_INSTRUMENTATION_FOR := KeyChain
+LOCAL_MODULE := KeyChainRoboTests
+
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+#############################################################
+# Settings runner target to run the previous target. #
+#############################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunKeyChainRoboTests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ KeyChainRoboTests
+
+LOCAL_TEST_PACKAGE := KeyChain
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
+
+LOCAL_ROBOTEST_TIMEOUT := 36000
+
+include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
diff --git a/robotests/src/com/android/keychain/AliasLoaderTest.java b/robotests/src/com/android/keychain/AliasLoaderTest.java
new file mode 100644
index 0000000..f2a05c8
--- /dev/null
+++ b/robotests/src/com/android/keychain/AliasLoaderTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.android.keychain;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.security.Credentials;
+import android.security.KeyStore;
+import com.android.keychain.internal.KeyInfoProvider;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public final class AliasLoaderTest {
+ private KeyInfoProvider mDummyInfoProvider;
+
+ @Before
+ public void setUp() {
+ mDummyInfoProvider =
+ new KeyInfoProvider() {
+ public boolean isUserSelectable(String alias) {
+ return true;
+ }
+ };
+ }
+
+ @Test
+ public void testAliasLoader_loadsAllAliases()
+ throws InterruptedException, ExecutionException, CancellationException,
+ TimeoutException {
+ KeyStore keyStore = mock(KeyStore.class);
+ when(keyStore.list(Credentials.USER_PRIVATE_KEY)).thenReturn(new String[] {"b", "c", "a"});
+
+ KeyChainActivity.AliasLoader loader =
+ new KeyChainActivity.AliasLoader(
+ keyStore, RuntimeEnvironment.application, mDummyInfoProvider);
+ loader.execute();
+
+ ShadowApplication.runBackgroundTasks();
+ KeyChainActivity.CertificateAdapter result = loader.get(5, TimeUnit.SECONDS);
+ Assert.assertNotNull(result);
+ Assert.assertEquals(3, result.getCount());
+ Assert.assertEquals("a", result.getItem(0));
+ Assert.assertEquals("b", result.getItem(1));
+ Assert.assertEquals("c", result.getItem(2));
+ }
+
+ @Test
+ public void testAliasLoader_copesWithNoAliases()
+ throws InterruptedException, ExecutionException, CancellationException,
+ TimeoutException {
+ KeyStore keyStore = mock(KeyStore.class);
+ when(keyStore.list(Credentials.USER_PRIVATE_KEY)).thenReturn(null);
+
+ KeyChainActivity.AliasLoader loader =
+ new KeyChainActivity.AliasLoader(
+ keyStore, RuntimeEnvironment.application, mDummyInfoProvider);
+ loader.execute();
+
+ ShadowApplication.runBackgroundTasks();
+ KeyChainActivity.CertificateAdapter result = loader.get(5, TimeUnit.SECONDS);
+ Assert.assertNotNull(result);
+ Assert.assertEquals(0, result.getCount());
+ }
+
+ @Test
+ public void testAliasLoader_filtersNonUserSelectableAliases()
+ throws InterruptedException, ExecutionException, CancellationException,
+ TimeoutException {
+ KeyStore keyStore = mock(KeyStore.class);
+ when(keyStore.list(Credentials.USER_PRIVATE_KEY)).thenReturn(new String[] {"a", "b", "c"});
+ KeyInfoProvider infoProvider = mock(KeyInfoProvider.class);
+ when(infoProvider.isUserSelectable("a")).thenReturn(false);
+ when(infoProvider.isUserSelectable("b")).thenReturn(true);
+ when(infoProvider.isUserSelectable("c")).thenReturn(false);
+
+ KeyChainActivity.AliasLoader loader =
+ new KeyChainActivity.AliasLoader(
+ keyStore, RuntimeEnvironment.application, infoProvider);
+ loader.execute();
+
+ ShadowApplication.runBackgroundTasks();
+ KeyChainActivity.CertificateAdapter result = loader.get(5, TimeUnit.SECONDS);
+ Assert.assertNotNull(result);
+ Assert.assertEquals(1, result.getCount());
+ Assert.assertEquals("b", result.getItem(0));
+ }
+}
diff --git a/robotests/src/com/android/keychain/TestConfig.java b/robotests/src/com/android/keychain/TestConfig.java
new file mode 100644
index 0000000..d31d2e5
--- /dev/null
+++ b/robotests/src/com/android/keychain/TestConfig.java
@@ -0,0 +1,22 @@
+/*
+ * 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.android.keychain;
+
+public class TestConfig {
+ public static final int SDK_VERSION = 26;
+ public static final String MANIFEST_PATH = "packages/apps/KeyChain/AndroidManifest.xml";
+}
diff --git a/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java b/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java
new file mode 100644
index 0000000..ecb0889
--- /dev/null
+++ b/robotests/src/com/android/keychain/internal/GrantsDatabaseTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.android.keychain.internal;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManager;
+import com.android.keychain.TestConfig;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Unit tests for {@link com.android.keychain.internal.GrantsDatabase}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public final class GrantsDatabaseTest {
+ private static final String DUMMY_ALIAS = "dummy_alias";
+ private static final String DUMMY_ALIAS2 = "another_dummy_alias";
+ private static final int DUMMY_UID = 1000;
+ private static final int DUMMY_UID2 = 1001;
+
+ private GrantsDatabase mGrantsDB;
+
+ @Before
+ public void setUp() {
+ mGrantsDB = new GrantsDatabase(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void testSetGrant_notMixingUIDs() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID2, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testSetGrant_notMixingAliases() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS2));
+ }
+
+ @Test
+ public void testSetGrantTrue() {
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testSetGrantFalse() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, false);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testSetGrantTrueThenFalse() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, false);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testRemoveGrantsForAlias() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ mGrantsDB.setGrant(DUMMY_UID2, DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ mGrantsDB.removeGrantsForAlias(DUMMY_ALIAS);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID2, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testRemoveAllGrants() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ mGrantsDB.setGrant(DUMMY_UID2, DUMMY_ALIAS, true);
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS2, true);
+ mGrantsDB.removeAllGrants();
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID2, DUMMY_ALIAS));
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS2));
+ }
+
+ @Test
+ public void testPurgeOldGrantsDoesNotDeleteGrantsForExistingPackages() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.getPackagesForUid(DUMMY_UID)).thenReturn(new String[]{"p"});
+ mGrantsDB.purgeOldGrants(pm);
+ Assert.assertTrue(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testPurgeOldGrantsPurgesAllNonExistingPackages() {
+ mGrantsDB.setGrant(DUMMY_UID, DUMMY_ALIAS, true);
+ mGrantsDB.setGrant(DUMMY_UID2, DUMMY_ALIAS, true);
+ PackageManager pm = mock(PackageManager.class);
+ when(pm.getPackagesForUid(DUMMY_UID)).thenReturn(null);
+ when(pm.getPackagesForUid(DUMMY_UID2)).thenReturn(null);
+ mGrantsDB.purgeOldGrants(pm);
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID, DUMMY_ALIAS));
+ Assert.assertFalse(mGrantsDB.hasGrant(DUMMY_UID2, DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testPurgeOldGrantsWorksOnEmptyDatabase() {
+ // Check that NPE is not thrown.
+ mGrantsDB.purgeOldGrants(null);
+ }
+
+ @Test
+ public void testIsUserSelectable() {
+ Assert.assertFalse(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
+ mGrantsDB.setIsUserSelectable(DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
+ }
+
+ @Test
+ public void testSetUserSelectable() {
+ mGrantsDB.setIsUserSelectable(DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
+ mGrantsDB.setIsUserSelectable(DUMMY_ALIAS, false);
+ Assert.assertFalse(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
+ mGrantsDB.setIsUserSelectable(DUMMY_ALIAS, true);
+ Assert.assertTrue(mGrantsDB.isUserSelectable(DUMMY_ALIAS));
+ }
+}
diff --git a/src/com/android/keychain/KeyChainActivity.java b/src/com/android/keychain/KeyChainActivity.java
index 3fd4a13..2eb7c89 100644
--- a/src/com/android/keychain/KeyChainActivity.java
+++ b/src/com/android/keychain/KeyChainActivity.java
@@ -46,6 +46,8 @@
import android.widget.ListView;
import android.widget.RadioButton;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.keychain.internal.KeyInfoProvider;
import com.android.org.bouncycastle.asn1.x509.X509Name;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -57,6 +59,7 @@
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.List;
+import java.util.stream.Collectors;
import javax.security.auth.x500.X500Principal;
@@ -144,7 +147,24 @@
private void chooseCertificate() {
// Start loading the set of certs to choose from now- if device policy doesn't return an
// alias, having aliases loading already will save some time waiting for UI to start.
- final AliasLoader loader = new AliasLoader();
+ KeyInfoProvider keyInfoProvider = new KeyInfoProvider() {
+ public boolean isUserSelectable(String alias) {
+ try (KeyChain.KeyChainConnection connection =
+ KeyChain.bind(KeyChainActivity.this)) {
+ return connection.getService().isUserSelectable(alias);
+ }
+ catch (InterruptedException ignored) {
+ Log.e(TAG, "interrupted while checking if key is user-selectable", ignored);
+ Thread.currentThread().interrupt();
+ return false;
+ } catch (Exception ignored) {
+ Log.e(TAG, "error while checking if key is user-selectable", ignored);
+ return false;
+ }
+ }
+ };
+
+ final AliasLoader loader = new AliasLoader(mKeyStore, this, keyInfoProvider);
loader.execute();
final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() {
@@ -192,14 +212,26 @@
}
}
- private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> {
+ @VisibleForTesting
+ static class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> {
+ private final KeyStore mKeyStore;
+ private final Context mContext;
+ private final KeyInfoProvider mInfoProvider;
+
+ public AliasLoader(KeyStore keyStore, Context context, KeyInfoProvider infoProvider) {
+ mKeyStore = keyStore;
+ mContext = context;
+ mInfoProvider = infoProvider;
+ }
+
@Override protected CertificateAdapter doInBackground(Void... params) {
String[] aliasArray = mKeyStore.list(Credentials.USER_PRIVATE_KEY);
- List<String> aliasList = ((aliasArray == null)
+ List<String> rawAliasList = ((aliasArray == null)
? Collections.<String>emptyList()
: Arrays.asList(aliasArray));
- Collections.sort(aliasList);
- return new CertificateAdapter(aliasList);
+ return new CertificateAdapter(mKeyStore, mContext,
+ rawAliasList.stream().filter(mInfoProvider::isUserSelectable).sorted()
+ .collect(Collectors.toList()));
}
}
@@ -323,12 +355,18 @@
dialog.show();
}
- private class CertificateAdapter extends BaseAdapter {
+ @VisibleForTesting
+ static class CertificateAdapter extends BaseAdapter {
private final List<String> mAliases;
private final List<String> mSubjects = new ArrayList<String>();
- private CertificateAdapter(List<String> aliases) {
+ private final KeyStore mKeyStore;
+ private final Context mContext;
+
+ private CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases) {
mAliases = aliases;
mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
+ mKeyStore = keyStore;
+ mContext = context;
}
@Override public int getCount() {
return mAliases.size();
@@ -342,7 +380,7 @@
@Override public View getView(final int adapterPosition, View view, ViewGroup parent) {
ViewHolder holder;
if (view == null) {
- LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this);
+ LayoutInflater inflater = LayoutInflater.from(mContext);
view = inflater.inflate(R.layout.cert_item, parent, false);
holder = new ViewHolder();
holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias);
@@ -457,6 +495,11 @@
if (mAlias != null) {
KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this);
try {
+ if (!connection.getService().isUserSelectable(mAlias)) {
+ Log.w(TAG, String.format("Alias %s not user-selectable.", mAlias));
+ //TODO: Should we invoke the callback with null here to indicate error?
+ return null;
+ }
connection.getService().setGrant(mSenderUid, mAlias, true);
} finally {
connection.close();
diff --git a/src/com/android/keychain/KeyChainService.java b/src/com/android/keychain/KeyChainService.java
index f45a1e6..09bdc6c 100644
--- a/src/com/android/keychain/KeyChainService.java
+++ b/src/com/android/keychain/KeyChainService.java
@@ -37,6 +37,7 @@
import android.security.KeyChain;
import android.security.KeyStore;
import android.util.Log;
+import com.android.keychain.internal.GrantsDatabase;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
@@ -54,25 +55,8 @@
private static final String TAG = "KeyChain";
- private static final String DATABASE_NAME = "grants.db";
- private static final int DATABASE_VERSION = 1;
- private static final String TABLE_GRANTS = "grants";
- private static final String GRANTS_ALIAS = "alias";
- private static final String GRANTS_GRANTEE_UID = "uid";
-
/** created in onCreate(), closed in onDestroy() */
- public DatabaseHelper mDatabaseHelper;
-
- private static final String SELECTION_COUNT_OF_MATCHING_GRANTS =
- "SELECT COUNT(*) FROM " + TABLE_GRANTS
- + " WHERE " + GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?";
-
- private static final String SELECT_GRANTS_BY_UID_AND_ALIAS =
- GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?";
-
- private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?";
-
- private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?";
+ public GrantsDatabase mGrantsDb;
public KeyChainService() {
super(KeyChainService.class.getSimpleName());
@@ -80,14 +64,14 @@
@Override public void onCreate() {
super.onCreate();
- mDatabaseHelper = new DatabaseHelper(this);
+ mGrantsDb = new GrantsDatabase(this);
}
@Override
public void onDestroy() {
super.onDestroy();
- mDatabaseHelper.close();
- mDatabaseHelper = null;
+ mGrantsDb.destroy();
+ mGrantsDb = null;
}
private final IKeyChainService.Stub mIKeyChainService = new IKeyChainService.Stub() {
@@ -114,17 +98,36 @@
return mKeyStore.get(Credentials.CA_CERTIFICATE + alias);
}
- private void checkArgs(String alias) {
+ @Override public boolean isUserSelectable(String alias) {
+ validateAlias(alias);
+ return mGrantsDb.isUserSelectable(alias);
+ }
+
+ @Override public void setUserSelectable(String alias, boolean isUserSelectable) {
+ validateAlias(alias);
+ checkSystemCaller();
+ mGrantsDb.setIsUserSelectable(alias, isUserSelectable);
+ }
+
+ private void validateAlias(String alias) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
+ }
+
+ private void validateKeyStoreState() {
if (!mKeyStore.isUnlocked()) {
throw new IllegalStateException("keystore is "
+ mKeyStore.state().toString());
}
+ }
+
+ private void checkArgs(String alias) {
+ validateAlias(alias);
+ validateKeyStoreState();
final int callingUid = getCallingUid();
- if (!hasGrantInternal(mDatabaseHelper.getReadableDatabase(), callingUid, alias)) {
+ if (!mGrantsDb.hasGrant(callingUid, alias)) {
throw new IllegalStateException("uid " + callingUid
+ " doesn't have permission to access the requested alias");
}
@@ -203,7 +206,7 @@
if (!Credentials.deleteAllTypesForAlias(mKeyStore, alias)) {
return false;
}
- removeGrantsForAlias(alias);
+ mGrantsDb.removeGrantsForAlias(alias);
broadcastKeychainChange();
broadcastLegacyStorageChange();
return true;
@@ -217,7 +220,7 @@
@Override public boolean reset() {
// only Settings should be able to reset
checkSystemCaller();
- removeAllGrants(mDatabaseHelper.getWritableDatabase());
+ mGrantsDb.removeAllGrants();
boolean ok = true;
synchronized (mTrustedCertificateStore) {
// delete user-installed CA certs
@@ -283,12 +286,12 @@
@Override public boolean hasGrant(int uid, String alias) {
checkSystemCaller();
- return hasGrantInternal(mDatabaseHelper.getReadableDatabase(), uid, alias);
+ return mGrantsDb.hasGrant(uid, alias);
}
@Override public void setGrant(int uid, String alias, boolean value) {
checkSystemCaller();
- setGrantInternal(mDatabaseHelper.getWritableDatabase(), uid, alias, value);
+ mGrantsDb.setGrant(uid, alias, value);
broadcastPermissionChange(uid, alias, value);
broadcastLegacyStorageChange();
}
@@ -359,60 +362,6 @@
}
};
- private boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) {
- final long numMatches = DatabaseUtils.longForQuery(db, SELECTION_COUNT_OF_MATCHING_GRANTS,
- new String[]{String.valueOf(uid), alias});
- return numMatches > 0;
- }
-
- private void setGrantInternal(final SQLiteDatabase db,
- final int uid, final String alias, final boolean value) {
- if (value) {
- if (!hasGrantInternal(db, uid, alias)) {
- final ContentValues values = new ContentValues();
- values.put(GRANTS_ALIAS, alias);
- values.put(GRANTS_GRANTEE_UID, uid);
- db.insert(TABLE_GRANTS, GRANTS_ALIAS, values);
- }
- } else {
- db.delete(TABLE_GRANTS, SELECT_GRANTS_BY_UID_AND_ALIAS,
- new String[]{String.valueOf(uid), alias});
- }
- }
-
- private void removeGrantsForAlias(String alias) {
- final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
- db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
- }
-
- private void removeAllGrants(final SQLiteDatabase db) {
- db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */);
- }
-
- private class DatabaseHelper extends SQLiteOpenHelper {
- public DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(final SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + TABLE_GRANTS + " ( "
- + GRANTS_ALIAS + " STRING NOT NULL, "
- + GRANTS_GRANTEE_UID + " INTEGER NOT NULL, "
- + "UNIQUE (" + GRANTS_ALIAS + "," + GRANTS_GRANTEE_UID + "))");
- }
-
- @Override
- public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
- Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
-
- if (oldVersion == 1) {
- // the first upgrade step goes here
- oldVersion++;
- }
- }
- }
-
@Override public IBinder onBind(Intent intent) {
if (IKeyChainService.class.getName().equals(intent.getAction())) {
return mIKeyChainService;
@@ -423,35 +372,7 @@
@Override
protected void onHandleIntent(final Intent intent) {
if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
- purgeOldGrants();
- }
- }
-
- private void purgeOldGrants() {
- final PackageManager packageManager = getPackageManager();
- final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
- Cursor cursor = null;
- db.beginTransaction();
- try {
- cursor = db.query(TABLE_GRANTS,
- new String[]{GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null);
- while (cursor.moveToNext()) {
- final int uid = cursor.getInt(0);
- final boolean packageExists = packageManager.getPackagesForUid(uid) != null;
- if (packageExists) {
- continue;
- }
- Log.d(TAG, "deleting grants for UID " + uid
- + " because its package is no longer installed");
- db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_UID,
- new String[]{Integer.toString(uid)});
- }
- db.setTransactionSuccessful();
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- db.endTransaction();
+ mGrantsDb.purgeOldGrants(getPackageManager());
}
}
diff --git a/src/com/android/keychain/internal/GrantsDatabase.java b/src/com/android/keychain/internal/GrantsDatabase.java
new file mode 100644
index 0000000..28605b5
--- /dev/null
+++ b/src/com/android/keychain/internal/GrantsDatabase.java
@@ -0,0 +1,213 @@
+/*
+ * 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.android.keychain.internal;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+public class GrantsDatabase {
+ private static final String TAG = "KeyChain";
+
+ private static final String DATABASE_NAME = "grants.db";
+ private static final int DATABASE_VERSION = 1;
+ private static final String TABLE_GRANTS = "grants";
+ private static final String GRANTS_ALIAS = "alias";
+ private static final String GRANTS_GRANTEE_UID = "uid";
+
+ private static final String SELECTION_COUNT_OF_MATCHING_GRANTS =
+ "SELECT COUNT(*) FROM "
+ + TABLE_GRANTS
+ + " WHERE "
+ + GRANTS_GRANTEE_UID
+ + "=? AND "
+ + GRANTS_ALIAS
+ + "=?";
+
+ private static final String SELECT_GRANTS_BY_UID_AND_ALIAS =
+ GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?";
+
+ private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?";
+
+ private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?";
+
+ private static final String TABLE_SELECTABLE = "userselectable";
+ private static final String SELECTABLE_IS_SELECTABLE = "is_selectable";
+ private static final String COUNT_SELECTABILITY_FOR_ALIAS =
+ "SELECT COUNT(*) FROM " + TABLE_SELECTABLE + " WHERE " + GRANTS_ALIAS + "=?";
+
+ public DatabaseHelper mDatabaseHelper;
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL(
+ "CREATE TABLE "
+ + TABLE_GRANTS
+ + " ( "
+ + GRANTS_ALIAS
+ + " STRING NOT NULL, "
+ + GRANTS_GRANTEE_UID
+ + " INTEGER NOT NULL, "
+ + "UNIQUE ("
+ + GRANTS_ALIAS
+ + ","
+ + GRANTS_GRANTEE_UID
+ + "))");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + TABLE_SELECTABLE
+ + " ( "
+ + GRANTS_ALIAS
+ + " STRING NOT NULL, "
+ + SELECTABLE_IS_SELECTABLE
+ + " STRING NOT NULL, "
+ + "UNIQUE ("
+ + GRANTS_ALIAS
+ + "))");
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
+ Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
+
+ if (oldVersion == 1) {
+ // the first upgrade step goes here
+ oldVersion++;
+ }
+ }
+ }
+
+ public GrantsDatabase(Context context) {
+ mDatabaseHelper = new DatabaseHelper(context);
+ }
+
+ public void destroy() {
+ mDatabaseHelper.close();
+ mDatabaseHelper = null;
+ }
+
+ boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) {
+ final long numMatches =
+ DatabaseUtils.longForQuery(
+ db,
+ SELECTION_COUNT_OF_MATCHING_GRANTS,
+ new String[] {String.valueOf(uid), alias});
+ return numMatches > 0;
+ }
+
+ public boolean hasGrant(final int uid, final String alias) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ return hasGrantInternal(db, uid, alias);
+ }
+
+ public void setGrant(final int uid, final String alias, final boolean value) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ if (value) {
+ if (!hasGrantInternal(db, uid, alias)) {
+ final ContentValues values = new ContentValues();
+ values.put(GRANTS_ALIAS, alias);
+ values.put(GRANTS_GRANTEE_UID, uid);
+ db.insert(TABLE_GRANTS, GRANTS_ALIAS, values);
+ }
+ } else {
+ db.delete(
+ TABLE_GRANTS,
+ SELECT_GRANTS_BY_UID_AND_ALIAS,
+ new String[] {String.valueOf(uid), alias});
+ }
+ }
+
+ public void removeGrantsForAlias(String alias) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
+ }
+
+ public void removeAllGrants() {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */);
+ }
+
+ public void purgeOldGrants(PackageManager pm) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ db.beginTransaction();
+ try (Cursor cursor = db.query(
+ TABLE_GRANTS,
+ new String[] {GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null)) {
+ while ((cursor != null) && (cursor.moveToNext())) {
+ final int uid = cursor.getInt(0);
+ final boolean packageExists = pm.getPackagesForUid(uid) != null;
+ if (packageExists) {
+ continue;
+ }
+ Log.d(TAG, String.format(
+ "deleting grants for UID %d because its package is no longer installed",
+ uid));
+ db.delete(
+ TABLE_GRANTS,
+ SELECTION_GRANTS_BY_UID,
+ new String[] {Integer.toString(uid)});
+ }
+ db.setTransactionSuccessful();
+ }
+
+ db.endTransaction();
+ }
+
+ public void setIsUserSelectable(final String alias, final boolean userSelectable) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ final ContentValues values = new ContentValues();
+ values.put(GRANTS_ALIAS, alias);
+ values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(userSelectable));
+
+ db.replace(TABLE_SELECTABLE, null, values);
+ }
+
+ public boolean isUserSelectable(final String alias) {
+ final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ try (Cursor res =
+ db.query(
+ TABLE_SELECTABLE,
+ new String[] {SELECTABLE_IS_SELECTABLE},
+ SELECTION_GRANTS_BY_ALIAS,
+ new String[] {alias},
+ null /* group by */,
+ null /* having */,
+ null /* order by */)) {
+ if (res == null || !res.moveToNext()) {
+ return false;
+ }
+
+ boolean isSelectable = Boolean.parseBoolean(res.getString(0));
+ if (!res.isAfterLast()) {
+ // BUG! Should not have more than one result for any given alias.
+ Log.w(TAG, String.format("Have more than one result for alias %s", alias));
+ }
+ return isSelectable;
+ }
+ }
+}
diff --git a/src/com/android/keychain/internal/KeyInfoProvider.java b/src/com/android/keychain/internal/KeyInfoProvider.java
new file mode 100644
index 0000000..c5d1e30
--- /dev/null
+++ b/src/com/android/keychain/internal/KeyInfoProvider.java
@@ -0,0 +1,14 @@
+package com.android.keychain.internal;
+
+/** Interface for classes that provide information about keys in KeyChain. */
+
+public interface KeyInfoProvider {
+ /**
+ * Indicates whether a key associated with the given alias is allowed
+ * to be selected by users.
+ * @param alias Alias of the key to check.
+ *
+ * @return true if the provided alias is selectable by users.
+ */
+ public boolean isUserSelectable(String alias);
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index dc60913..fa9cd55 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -20,8 +20,19 @@
<uses-permission android:name="android.permission.INTERNET"/>
<!--
+ Install the activity and disable battery optimization (so the KeyChainServiceTest can be
+ run in the background):
+ adb install out/target/product/${TARGET_PRODUCT}/data/app/KeyChainTests/KeyChainTests.apk
+ Then navigate to Settings -> Battery -> ... -> Battery optimization -> select All Apps
+ Find com.android.keychain.tests and select Do Not Optimize.
+
+ Alternatively, the following command can be used to exclude the test services from
+ the background execution restriction for 2 minutes:
+ adb shell cmd deviceidle tempwhitelist -d 120000 com.android.keychain.tests
+
To run service:
adb shell am startservice -n com.android.keychain.tests/.KeyChainServiceTest
+ One has to inspect the ADB log to find out about test failures.
To run activity:
adb shell am start -n com.android.keychain.tests/com.android.keychain.tests.KeyChainTestActivity
diff --git a/tests/src/com/android/keychain/tests/KeyChainServiceTest.java b/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
index 1f3f7de..7e4008a 100644
--- a/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
+++ b/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
@@ -21,6 +21,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.os.IBinder;
import android.security.Credentials;
import android.security.IKeyChainService;
@@ -75,16 +77,36 @@
}
};
+ private static void addComponentToIntent(PackageManager pm, Intent intent) {
+ ResolveInfo service = pm.resolveService(intent, 0);
+ if (service == null) {
+ Log.w(TAG, String.format("No service found for intent: %s", intent.getAction()));
+ } else {
+ Log.d(TAG, String.format("Found service: %s %s for action %s",
+ service.serviceInfo.packageName, service.serviceInfo.name,
+ intent.getAction()));
+ ComponentName comp = new ComponentName(
+ service.serviceInfo.packageName, service.serviceInfo.name);
+ intent.setComponent(comp);
+ }
+ }
+
private void bindSupport() {
- mIsBoundSupport = bindService(new Intent(IKeyChainServiceTestSupport.class.getName()),
+ Intent serviceIntent = new Intent(IKeyChainServiceTestSupport.class.getName());
+ addComponentToIntent(getPackageManager(), serviceIntent);
+ mIsBoundSupport = bindService(serviceIntent,
mSupportConnection,
Context.BIND_AUTO_CREATE);
+ Log.d(TAG, String.format("Finished bindSupport with result: %b", mIsBoundSupport));
}
private void bindService() {
- mIsBoundService = bindService(new Intent(IKeyChainService.class.getName()),
+ Intent serviceIntent = new Intent(IKeyChainService.class.getName());
+ addComponentToIntent(getPackageManager(), serviceIntent);
+ mIsBoundService = bindService(serviceIntent,
mServiceConnection,
Context.BIND_AUTO_CREATE);
+ Log.d(TAG, String.format("Finished bindService with result: %b", mIsBoundService));
}
private void unbindServices() {