From 1af6bb1465ed23c5d001305e9d089cf44fa73feb Mon Sep 17 00:00:00 2001 From: Niharika Arora Date: Wed, 14 Feb 2024 11:22:02 +0530 Subject: [PATCH] Added sample provider implementation Change-Id: I2d96b5b0083f6f1b8c82d2bcd024684c6eaf42df --- CredentialProvider/MyVault/.editorconfig | 7 + CredentialProvider/MyVault/LICENSE | 202 +++++++ .../MyVault}/README.md | 0 CredentialProvider/MyVault/app/.gitignore | 1 + .../MyVault/app/build.gradle.kts | 96 ++++ .../MyVault/app/proguard-rules.pro | 21 + .../MyVault/app/src/main/AndroidManifest.xml | 109 ++++ .../authentication/myvault/AppDependencies.kt | 76 +++ .../authentication/myvault/Dimensions.kt | 29 + .../myvault/MyVaultApplication.kt | 28 + .../myvault/data/CredentialsDataSource.kt | 147 +++++ .../myvault/data/CredentialsRepository.kt | 337 +++++++++++ .../myvault/data/MyVaultService.kt | 221 +++++++ .../myvault/data/PasskeyItem.kt | 50 ++ .../myvault/data/PasswordItem.kt | 44 ++ .../myvault/data/RPIconDataSource.kt | 128 +++++ .../myvault/data/room/MyVaultDatabase.kt | 91 +++ .../myvault/data/room/SiteMetaData.kt | 38 ++ .../myvault/data/room/SiteWithCredentials.kt | 39 ++ .../myvault/fido/AssetLinkVerifier.kt | 104 ++++ .../fido/AuthenticatorAssertionResponse.kt | 109 ++++ .../fido/AuthenticatorAttestationResponse.kt | 138 +++++ .../myvault/fido/AuthenticatorResponse.kt | 30 + .../authentication/myvault/fido/Cbor.kt | 101 ++++ .../myvault/fido/FidoDataTypes.kt | 49 ++ .../myvault/fido/FidoPublicKeyCredential.kt | 59 ++ .../PublicKeyCredentialCreationOptions.kt | 70 +++ .../fido/PublicKeyCredentialRequestOptions.kt | 43 ++ .../authentication/myvault/fido/Util.kt | 55 ++ .../authentication/myvault/ui/AppDrawer.kt | 157 +++++ .../myvault/ui/CreatePasskeyActivity.kt | 538 ++++++++++++++++++ .../myvault/ui/CreatePasswordActivity.kt | 159 ++++++ .../myvault/ui/GetPasskeyActivity.kt | 503 ++++++++++++++++ .../myvault/ui/GetPasswordActivity.kt | 138 +++++ .../authentication/myvault/ui/MainActivity.kt | 36 ++ .../myvault/ui/MyVaultAppNavigation.kt | 115 ++++ .../myvault/ui/MyVaultNavActions.kt | 57 ++ .../myvault/ui/MyVaultNavGraph.kt | 70 +++ .../myvault/ui/UnlockActivity.kt | 134 +++++ .../myvault/ui/home/CredentialsList.kt | 173 ++++++ .../myvault/ui/home/HomeScreen.kt | 195 +++++++ .../myvault/ui/home/HomeViewModel.kt | 87 +++ .../myvault/ui/home/HomeViewModelFactory.kt | 37 ++ .../myvault/ui/home/ShowCredentialsScreen.kt | 345 +++++++++++ .../myvault/ui/password/PasswordScreen.kt | 156 +++++ .../myvault/ui/settings/SettingsScreen.kt | 195 +++++++ .../myvault/ui/settings/SettingsViewModel.kt | 57 ++ .../ui/settings/SettingsViewModelFactory.kt | 36 ++ .../authentication/myvault/ui/theme/Color.kt | 78 +++ .../authentication/myvault/ui/theme/Shape.kt | 27 + .../authentication/myvault/ui/theme/Theme.kt | 107 ++++ .../myvault/ui/theme/Typography.kt | 25 + .../src/main/res/drawable/android_secure.xml | 27 + .../app/src/main/res/values/strings.xml | 64 +++ .../app/src/main/res/values/themes.xml | 19 + .../MyVault/app/src/main/res/xml/provider.xml | 25 + CredentialProvider/MyVault/build.gradle.kts | 6 + .../images/credentials-in-client-sample.png | Bin 0 -> 33109 bytes .../MyVault/docs/images/credentials-list.png | Bin 0 -> 29680 bytes .../docs/images/passkey-credentials.png | Bin 0 -> 20196 bytes .../docs/images/password-credentials.png | Bin 0 -> 23364 bytes .../docs/images/save-passkey-in-my-vault.png | Bin 0 -> 40098 bytes .../docs/images/save-password-in-myvault.png | Bin 0 -> 37543 bytes CredentialProvider/MyVault/gradle.properties | 23 + .../MyVault/gradle/libs.versions.toml | 51 ++ .../MyVault/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + CredentialProvider/MyVault/gradlew | 185 ++++++ CredentialProvider/MyVault/gradlew.bat | 89 +++ CredentialProvider/MyVault/init.gradle.kts | 53 ++ .../MyVault/settings.gradle.kts | 24 + .../MyVault/spotless/copyright.kt | 15 + .../MyVault/spotless/copyright.kts | 15 + 73 files changed, 6449 insertions(+) create mode 100644 CredentialProvider/MyVault/.editorconfig create mode 100644 CredentialProvider/MyVault/LICENSE rename {MyVault => CredentialProvider/MyVault}/README.md (100%) create mode 100644 CredentialProvider/MyVault/app/.gitignore create mode 100644 CredentialProvider/MyVault/app/build.gradle.kts create mode 100644 CredentialProvider/MyVault/app/proguard-rules.pro create mode 100644 CredentialProvider/MyVault/app/src/main/AndroidManifest.xml create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/Dimensions.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/MyVaultApplication.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsDataSource.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/MyVaultService.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasskeyItem.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasswordItem.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/RPIconDataSource.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/MyVaultDatabase.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteMetaData.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteWithCredentials.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AssetLinkVerifier.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAssertionResponse.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAttestationResponse.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorResponse.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Cbor.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoDataTypes.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoPublicKeyCredential.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialCreationOptions.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialRequestOptions.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Util.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/AppDrawer.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasskeyActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasswordActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasskeyActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasswordActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MainActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultAppNavigation.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavActions.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavGraph.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/UnlockActivity.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/CredentialsList.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeScreen.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModel.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModelFactory.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/ShowCredentialsScreen.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/password/PasswordScreen.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsScreen.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModel.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModelFactory.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Color.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Shape.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Theme.kt create mode 100644 CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Typography.kt create mode 100644 CredentialProvider/MyVault/app/src/main/res/drawable/android_secure.xml create mode 100644 CredentialProvider/MyVault/app/src/main/res/values/strings.xml create mode 100644 CredentialProvider/MyVault/app/src/main/res/values/themes.xml create mode 100644 CredentialProvider/MyVault/app/src/main/res/xml/provider.xml create mode 100644 CredentialProvider/MyVault/build.gradle.kts create mode 100644 CredentialProvider/MyVault/docs/images/credentials-in-client-sample.png create mode 100644 CredentialProvider/MyVault/docs/images/credentials-list.png create mode 100644 CredentialProvider/MyVault/docs/images/passkey-credentials.png create mode 100644 CredentialProvider/MyVault/docs/images/password-credentials.png create mode 100644 CredentialProvider/MyVault/docs/images/save-passkey-in-my-vault.png create mode 100644 CredentialProvider/MyVault/docs/images/save-password-in-myvault.png create mode 100644 CredentialProvider/MyVault/gradle.properties create mode 100644 CredentialProvider/MyVault/gradle/libs.versions.toml create mode 100644 CredentialProvider/MyVault/gradle/wrapper/gradle-wrapper.jar create mode 100644 CredentialProvider/MyVault/gradle/wrapper/gradle-wrapper.properties create mode 100755 CredentialProvider/MyVault/gradlew create mode 100644 CredentialProvider/MyVault/gradlew.bat create mode 100644 CredentialProvider/MyVault/init.gradle.kts create mode 100644 CredentialProvider/MyVault/settings.gradle.kts create mode 100644 CredentialProvider/MyVault/spotless/copyright.kt create mode 100644 CredentialProvider/MyVault/spotless/copyright.kts diff --git a/CredentialProvider/MyVault/.editorconfig b/CredentialProvider/MyVault/.editorconfig new file mode 100644 index 0000000..6e88995 --- /dev/null +++ b/CredentialProvider/MyVault/.editorconfig @@ -0,0 +1,7 @@ +# https://editorconfig.org/ +# This configuration is used by ktlint when spotless invokes it + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma=true +ij_kotlin_allow_trailing_comma_on_call_site=true +ktlint_function_naming_ignore_when_annotated_with=Composable, Test \ No newline at end of file diff --git a/CredentialProvider/MyVault/LICENSE b/CredentialProvider/MyVault/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/CredentialProvider/MyVault/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/MyVault/README.md b/CredentialProvider/MyVault/README.md similarity index 100% rename from MyVault/README.md rename to CredentialProvider/MyVault/README.md diff --git a/CredentialProvider/MyVault/app/.gitignore b/CredentialProvider/MyVault/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/CredentialProvider/MyVault/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/CredentialProvider/MyVault/app/build.gradle.kts b/CredentialProvider/MyVault/app/build.gradle.kts new file mode 100644 index 0000000..6ff5074 --- /dev/null +++ b/CredentialProvider/MyVault/app/build.gradle.kts @@ -0,0 +1,96 @@ +/* + * Copyright 2024 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 + * + * https://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. + */ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.devtools.ksp) +} + +android { + namespace = "com.example.android.authentication.myvault" + + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.android.authentication.myvault" + minSdk = 34 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.10" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.credential.manager) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + annotationProcessor(libs.androidx.room.compiler) + implementation(libs.androidx.biometrics) + implementation(libs.androidx.navigation) + implementation(libs.google.accompanist) + implementation(libs.androidx.lifecyle.runtime.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/CredentialProvider/MyVault/app/proguard-rules.pro b/CredentialProvider/MyVault/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/CredentialProvider/MyVault/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/CredentialProvider/MyVault/app/src/main/AndroidManifest.xml b/CredentialProvider/MyVault/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..907bc0f --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/AndroidManifest.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt new file mode 100644 index 0000000..bc61a9a --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.drawable.Icon +import androidx.room.Room +import com.example.android.authentication.myvault.data.CredentialsDataSource +import com.example.android.authentication.myvault.data.CredentialsRepository +import com.example.android.authentication.myvault.data.RPIconDataSource +import com.example.android.authentication.myvault.data.room.MyVaultDatabase + +/** + * This class is an application-level singleton object which is providing dependencies required for the app to function. + * We recommend using dependency injection framework while working on production apps. + */ +object AppDependencies { + lateinit var database: MyVaultDatabase + lateinit var sharedPreferences: SharedPreferences + lateinit var credentialsRepository: CredentialsRepository + val credentialsDataSource by lazy { + CredentialsDataSource( + myVaultDao = database.myVaultDao(), + ) + } + + var providerIcon: Icon? = null + + lateinit var RPIconDataSource: RPIconDataSource + + /** + * Initializes the core components required for the application's data storage and icon handling. + * This includes: + * * **sharedPreference:** Creates a sharedpreference instance for storing application metadata. + * * **database:** Creates a Room database instance for storing application data. + * * **RPIconDataSource:** Initializes a data source for handling Relying Party icons (rpicons). + * * **provider icon:** Sets a default icon to represent secure data providers. + * + * @param context The application context, used for accessing resources and file storage. + */ + fun init(context: Context) { + sharedPreferences = context.getSharedPreferences( + context.packageName, + Context.MODE_PRIVATE, + ) + + database = Room.databaseBuilder(context, MyVaultDatabase::class.java, "my_vault.db") + .allowMainThreadQueries() + .fallbackToDestructiveMigration() + .build() + + RPIconDataSource = RPIconDataSource(context.applicationInfo.dataDir) + providerIcon = Icon.createWithResource(context, R.drawable.android_secure) + + credentialsRepository = + CredentialsRepository( + sharedPreferences, + credentialsDataSource, + context, + ) + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/Dimensions.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/Dimensions.kt new file mode 100644 index 0000000..ba53185 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/Dimensions.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault + +import androidx.compose.ui.unit.dp + +/** + * Provides a centralized collection of dimension and font size values used throughout + * the application. This promotes consistency and simplifies UI maintenance. + */ +object Dimensions { + val padding_small = 4.dp + val padding_medium = 8.dp + val padding_large = 16.dp + val padding_extra_large = 20.dp +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/MyVaultApplication.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/MyVaultApplication.kt new file mode 100644 index 0000000..03af831 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/MyVaultApplication.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault + +import android.app.Application + +/** + * This is the application level class used to initialize application level dependencies + */ +class MyVaultApplication : Application() { + override fun onCreate() { + super.onCreate() + AppDependencies.init(this) + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsDataSource.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsDataSource.kt new file mode 100644 index 0000000..324132c --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsDataSource.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import com.example.android.authentication.myvault.data.room.MyVaultDao +import com.example.android.authentication.myvault.data.room.SiteMetaData +import com.example.android.authentication.myvault.data.room.SiteWithCredentials +import kotlinx.coroutines.flow.Flow +import java.time.Instant + +/** + * This class is responsible for providing utility methods for updating credentials in database + */ +class CredentialsDataSource( + private val myVaultDao: MyVaultDao, +) { + + fun siteListWithCredentials(): Flow> { + return myVaultDao.siteListWithCredentials() + } + + fun credentialsForSite(url: String?): SiteWithCredentials? { + if (url == null) { + return null + } + return myVaultDao.getCredentialsFromSite(url) + } + + fun getPasskeysCount(siteId: String?): Int { + if (siteId == null) { + return 0 + } + + val credentialsFromSite = myVaultDao.getCredentialsFromSite(siteId) + + if (credentialsFromSite != null) { + return credentialsFromSite.passkeys.size + } + + return 0 + } + + fun getPasswordCount(url: String?): Int { + if (url == null) { + return 0 + } + + val credentialsFromSite = myVaultDao.getCredentialsFromSite(url) + + if (credentialsFromSite != null) { + return credentialsFromSite.passwords.size + } + return 0 + } + + private suspend fun addSite(siteMetaData: SiteMetaData): Long { + return myVaultDao.insertSite(siteMetaData) + } + + suspend fun updatePassword(password: PasswordItem) { + myVaultDao.updatePassword(password) + } + + suspend fun updatePasskey(passkey: PasskeyItem) { + myVaultDao.updatePasskey(passkey) + } + + suspend fun removePassword(password: PasswordItem) { + val siteId = password.siteId + myVaultDao.deletePassword(password) + if (myVaultDao.count(siteId) == 0) { + myVaultDao.deleteSite(SiteMetaData(id = siteId)) + } + } + + suspend fun removePasskey(passkey: PasskeyItem) { + val siteId = passkey.siteId + myVaultDao.deletePasskey(passkey) + if (myVaultDao.countPasskeys(siteId) == 0) { + myVaultDao.deleteSite(SiteMetaData(id = siteId)) + } + } + + suspend fun addNewPassword(passwordMetaData: PasswordMetaData) { + val site = myVaultDao.getSite(passwordMetaData.url) + val siteId = site?.id ?: addSite(SiteMetaData(url = passwordMetaData.url, name = "")) + myVaultDao.insertPassword( + PasswordItem( + username = passwordMetaData.username, + password = passwordMetaData.password, + siteId = siteId, + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + + suspend fun addNewPasskey(passkeyMetadata: PasskeyMetadata) { + val site = myVaultDao.getSite(passkeyMetadata.rpid) + val siteId = site?.id ?: addSite(SiteMetaData(url = passkeyMetadata.rpid, name = "")) + myVaultDao.insertPasskey( + PasskeyItem( + uid = passkeyMetadata.uid, + username = passkeyMetadata.username, + displayName = passkeyMetadata.displayName, + credId = passkeyMetadata.credId, + credPrivateKey = passkeyMetadata.credPrivateKey, + siteId = siteId, + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + + fun getPasskey(credId: String): PasskeyItem? { + return myVaultDao.getPasskey(credId) + } +} + +data class PasswordMetaData( + val username: String, + val password: String, + val url: String, + val name: String = "", + val lastUsedTimeMs: Long, +) + +data class PasskeyMetadata( + val uid: String, + val rpid: String, + val username: String, + val displayName: String, + val credId: String, + val credPrivateKey: String, + val lastUsedTimeMs: Long, +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt new file mode 100644 index 0000000..083ac8a --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt @@ -0,0 +1,337 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import androidx.credentials.provider.BeginCreateCredentialRequest +import androidx.credentials.provider.BeginCreateCredentialResponse +import androidx.credentials.provider.BeginCreatePasswordCredentialRequest +import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse.Builder +import androidx.credentials.provider.BeginGetPasswordOption +import androidx.credentials.provider.BeginGetPublicKeyCredentialOption +import androidx.credentials.provider.CreateEntry +import androidx.credentials.provider.PasswordCredentialEntry +import androidx.credentials.provider.PublicKeyCredentialEntry +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.fido.PublicKeyCredentialRequestOptions +import org.json.JSONObject +import java.io.IOException +import java.time.Instant +import java.util.concurrent.atomic.AtomicInteger + +/** + * This class is responsible for creating and retrieving credential entries (password & passkey) to & from the database + */ +class CredentialsRepository( + private val sharedPreferences: SharedPreferences, + private val credentialsDataSource: CredentialsDataSource, + private val applicationContext: Context, +) { + private val requestCode: AtomicInteger = AtomicInteger() + + /** + * This method queries credentials from your database, create passkey and password entries to populate. + * + * @param request The BeginGetPublicKeyCredentialOption object containing the request parameters. + * @param responseBuilder The Builder object used to build the BeginGetCredentialResponse. + * @return True if credentials were found and added to the response builder, false otherwise. + */ + fun processGetCredentialsRequest( + request: BeginGetCredentialRequest, + responseBuilder: Builder, + ): Boolean { + val callingPackage = request.callingAppInfo?.packageName ?: return false + + var hasFoundCredentials = false + + for (option in request.beginGetCredentialOptions) { + when (option) { + // If the chosen option is a Password credential + is BeginGetPasswordOption -> { + if (populatePasswordData(callingPackage, option, responseBuilder)) { + hasFoundCredentials = true + } + } + + // If the chosen option is a Passkey credential + is BeginGetPublicKeyCredentialOption -> { + if (populatePasskeyData(option, responseBuilder)) { + hasFoundCredentials = true + } + } + } + } + return hasFoundCredentials + } + + /** + * This method queries credentials from the storage used i.e database here, create passkey entries to populate. + * + * @param request The BeginCreateCredentialRequest object containing the request parameters. + * @return The BeginCreateCredentialResponse object containing the list of credential entries. + */ + fun processCreateCredentialsRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? { + var passwordCount = 0 + var passkeyCount = 0 + + val requestJson = + request.candidateQueryData.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON") + + val callingPackage = request.callingAppInfo?.packageName + if (!callingPackage.isNullOrEmpty()) { + passwordCount = credentialsDataSource.getPasswordCount(callingPackage) + } + + // Parse the request options into a PublicKeyCredentialRequestOptions object. + if (!requestJson.isNullOrEmpty()) { + val requestJsonObject = JSONObject(requestJson) + val rp: JSONObject = requestJsonObject.getJSONObject("rp") + val id: String = rp.getString("id") + passkeyCount = credentialsDataSource.getPasskeysCount(id) + } + + when (request) { + // Handle Password credential + is BeginCreatePasswordCredentialRequest -> { + return handleCreateCredentialQuery( + passwordCount, + passkeyCount, + CREATE_PASSWORD_INTENT, + ) + } + + // Handle Passkey credential + is BeginCreatePublicKeyCredentialRequest -> { + return handleCreateCredentialQuery( + passwordCount, + passkeyCount, + CREATE_PASSKEY_INTENT, + ) + } + } + return null + } + + /** + * This method queries credentials from the storage used i.e database here, create password entries to populate. + * + * @param callingPackage The package name of the calling app. + * @param option The BeginGetPasswordOption object containing the request parameters. + * @param responseBuilder The Builder object used to build the BeginGetCredentialResponse. + * @return True if credentials were found and added to the response builder, false otherwise. + */ + private fun populatePasswordData( + callingPackage: String, + option: BeginGetPasswordOption, + responseBuilder: Builder, + ): Boolean { + try { + val credentials = + credentialsDataSource.credentialsForSite(callingPackage) ?: return false + val passwords = credentials.passwords + val it = passwords.iterator() + while (it.hasNext()) { + val passwordItemCurrent = it.next() + + // Create Password entry + val entry = PasswordCredentialEntry.Builder( + applicationContext, + passwordItemCurrent.username, + createNewPendingIntent( + passwordItemCurrent.username, + GET_PASSWORD_INTENT, + ), + option, + ) + .setDisplayName("display-${passwordItemCurrent.username}") + .setIcon(AppDependencies.providerIcon!!) + .setLastUsedTime(Instant.ofEpochMilli(passwordItemCurrent.lastUsedTimeMs)) + .build() + // Add the entry to the response builder. + responseBuilder.addCredentialEntry(entry) + } + } catch (e: IOException) { + return false + } + return true + } + + /** + * This method queries credentials from your database, create passkey and password entries to populate. + * + * @param option The BeginGetPublicKeyCredentialOption object containing the request parameters. + * @param responseBuilder The Builder object used to build the BeginGetCredentialResponse. + * @return True if credentials were found and added to the response builder, false otherwise. + */ + private fun populatePasskeyData( + option: BeginGetPublicKeyCredentialOption, + responseBuilder: Builder, + ): Boolean { + try { + // Parse the request options into a PublicKeyCredentialRequestOptions object. + val request = PublicKeyCredentialRequestOptions(option.requestJson) + + // Get the credentials for the site specified in the request. + val credentials = credentialsDataSource.credentialsForSite(request.rpId) ?: return false + + val passkeys = credentials.passkeys + for (passkey in passkeys) { + val data = Bundle() + data.putString("requestJson", option.requestJson) + data.putString("credId", passkey.credId) + + // Create a PendingIntent to launch the activity that will handle the passkey retrieval + val pendingIntent = createNewPendingIntent( + "", + GET_PASSKEY_INTENT, + data, + ) + + // Create a PublicKeyCredentialEntry object to represent the passkey + val entryBuilder = PublicKeyCredentialEntry.Builder( + applicationContext, + passkey.username, + pendingIntent, + option, + ) + .setDisplayName(passkey.displayName) + .setLastUsedTime(Instant.ofEpochMilli(passkey.lastUsedTimeMs)) + .setIcon(AppDependencies.providerIcon!!) + + val entry = entryBuilder + .build() + responseBuilder.addCredentialEntry(entry) + } + } catch (e: IOException) { + return false + } + return true + } + + /** + * Creates a new PendingIntent for the given action and account ID. + * + * Any required data that the provider needs when the corresponding activity is invoked should be + * set as an extra on the intent that's used to create your PendingIntent, such as an accountId in the creation flow. + * + * @param accountId The ID of the account to associate with the PendingIntent. + * @param action The action to be performed when the PendingIntent is invoked. + * @param extra Optional Bundle containing additional data to be passed to the activity. + * @return A new PendingIntent. + */ + private fun createNewPendingIntent( + accountId: String, + action: String, + extra: Bundle? = null, + ): PendingIntent { + val intent = Intent(action).setPackage(applicationContext.packageName) + if (extra != null) { + intent.putExtra("VAULT_DATA", extra) + } + intent.putExtra(KEY_ACCOUNT_ID, accountId) + return PendingIntent.getActivity( + applicationContext, + requestCode.incrementAndGet(), + intent, + (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT), + ) + } + + fun getRequestCounter(): AtomicInteger { + return requestCode + } + + /** + * Adds a CreateEntry to the BeginCreateCredentialResponse. + * + * Each CreateEntry should correspond to an account where the credential can be saved, + * and must have a PendingIntent set along with other required metadata. + * + * @param passwordCount The number of password credentials associated with the account. + * @param passkeyCount The number of passkey credentials associated with the account. + * @param intentType The type of intent to be used for the PendingIntent. + * @return A BeginCreateCredentialResponse with the CreateEntry added. + */ + private fun handleCreateCredentialQuery( + passwordCount: Int, + passkeyCount: Int, + intentType: String, + ): BeginCreateCredentialResponse { + // Each CreateEntry should correspond to an account where the credential can be saved, + // and must have a PendingIntent set along with other required metadata. + return BeginCreateCredentialResponse.Builder() + .addCreateEntry( + createEntry( + intentType, + passwordCount, + passkeyCount, + ), + ).build() + } + + /** + * Creates a CreateEntry object for the user account based on their credential preferences. + * + * @param intentType The type of intent to be used for the PendingIntent. + * @param passwordCount The number of password credentials associated with the account. + * @param passkeyCount The number of passkey credentials associated with the account. + * @return A CreateEntry object. + */ + private fun createEntry( + intentType: String, + passwordCount: Int, + passkeyCount: Int, + ) = CreateEntry.Builder( + USER_ACCOUNT, + createNewPendingIntent(USER_ACCOUNT, intentType), + ).setLastUsedTime( + Instant.ofEpochMilli( + sharedPreferences.getLong( + KEY_ACCOUNT_LAST_USED_MS, + 0L, + ), + ), + ) + .setPasswordCredentialCount(passwordCount) + .setPublicKeyCredentialCount(passkeyCount) + .setTotalCredentialCount(passwordCount + passkeyCount) + .setDescription( + CREDENTIAL_DESCRIPTION, + ) + .build() + + companion object { + private const val CREATE_PASSWORD_INTENT = + "com.example.android.authentication.myvault.CREATE_PASSWORD" + private const val CREATE_PASSKEY_INTENT = + "com.example.android.authentication.myvault.CREATE_PASSKEY" + private const val GET_PASSKEY_INTENT = + "com.example.android.authentication.myvault.GET_PASSKEY" + private const val GET_PASSWORD_INTENT = + "com.example.android.authentication.myvault.GET_PASSWORD" + const val KEY_ACCOUNT_LAST_USED_MS = "key_account_last_used_ms" + const val KEY_ACCOUNT_ID = "key_account_id" + const val USER_ACCOUNT = "user_account" + const val CREDENTIAL_DESCRIPTION = + "Your credential will be saved securely to the chosen account." + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/MyVaultService.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/MyVaultService.kt new file mode 100644 index 0000000..d24f5d3 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/MyVaultService.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import android.app.PendingIntent +import android.content.Intent +import android.os.CancellationSignal +import android.os.OutcomeReceiver +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.CreateCredentialUnknownException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.exceptions.NoCredentialException +import androidx.credentials.provider.Action +import androidx.credentials.provider.AuthenticationAction +import androidx.credentials.provider.BeginCreateCredentialRequest +import androidx.credentials.provider.BeginCreateCredentialResponse +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse +import androidx.credentials.provider.CredentialProviderService +import androidx.credentials.provider.ProviderClearCredentialStateRequest +import androidx.credentials.provider.ProviderGetCredentialRequest +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.R +import java.io.IOException +import java.util.concurrent.atomic.AtomicInteger + +/* +* This class extends CredentialProviderService() that provides abstract methods (to be implemented) used to save and retrieve credentials for a given user, +* upon the request of a client app that typically uses these credentials for sign-in flows. +* +* The credential retrieval and creation/saving is mediated by the Android System that + * aggregates credentials from multiple credential provider services, and presents them to + * the user in the form of a selector UI for credential selections/account selections/ + * confirmations etc. + * + */ +class MyVaultService(private val credentialsRepository: CredentialsRepository = AppDependencies.credentialsRepository) : + CredentialProviderService() { + + + /** + * Called by the Android System in response to a client app calling + * [androidx.credentials.CredentialManager.createCredential], to create/save a credential + * with a credential provider installed on the device. + * + * @param [request] the [BeginCreateCredentialRequest] to handle + * See [BeginCreateCredentialResponse] for the response to be returned + * @param cancellationSignal signal for observing cancellation requests. The system will + * use this to notify you that the result is no longer needed and you should stop + * handling it in order to save your resources + * @param callback the callback object to be used to notify the response or error + */ + override fun onBeginCreateCredentialRequest( + request: BeginCreateCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver, + ) { + // Handle the BeginCreateCredentialRequest by constructing a corresponding BeginCreateCredentialResponse and passing it through the callback. + val response: BeginCreateCredentialResponse? = + credentialsRepository.processCreateCredentialsRequest(request) + if (response != null) { + callback.onResult(response) + } else { + callback.onError( + CreateCredentialUnknownException(), + ) + } + } + + /** + * Called by the Android System in response to a client app calling + * [androidx.credentials.CredentialManager.getCredential], to get a credential + * sourced from a credential provider installed on the device. + * + * @param [request] the [ProviderGetCredentialRequest] to handle + * See [BeginGetCredentialResponse] for the response to be returned + * @param cancellationSignal signal for observing cancellation requests. The system will + * use this to notify you that the result is no longer needed and you should stop + * handling it in order to save your resources + * @param callback the callback object to be used to notify the response or error + */ + override fun onBeginGetCredentialRequest( + request: BeginGetCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver, + ) { + val callingPackage = request.callingAppInfo?.packageName + if (callingPackage == null) { + callback.onError(NoCredentialException()) + } + + // Turn this to true if you want your app to be locked during every launch + val appLocked = false + val responseBuilder = BeginGetCredentialResponse.Builder() + + // Note that if your credentials are locked, you can immediately set an AuthenticationAction on the response and invoke the callback. + if (appLocked) { + callback.onResult( + responseBuilder.setAuthenticationActions( + listOf( + AuthenticationAction( + // Providers that require unlocking the credentials before returning any credentialEntries, + // must set up a pending intent that navigates the user to the app's unlock flow. + applicationContext.getString(R.string.app_name), + createPendingIntent( + credentialsRepository.getRequestCounter(), + UNLOCK_INTENT, + ), + ), + ), + ).build(), + ) + return + } + + val hasCredentialsFound = + credentialsRepository.processGetCredentialsRequest(request, responseBuilder) + val hasActionsPopulated = + populateActions(responseBuilder, credentialsRepository.getRequestCounter()) + + if (hasCredentialsFound || hasActionsPopulated) { + callback.onResult( + responseBuilder.build(), + ) + return + } + + callback.onError( + GetCredentialUnknownException(), + ) + } + + /** + * Called by the Android System in response to a client app calling + * [androidx.credentials.CredentialManager.clearCredentialState]. A client app typically + * calls this API on instances like sign-out when the intention is that the providers clear + * any state that they may have maintained for the given user. + * + * You should invoked this api after your user signs out of your app to notify all credential + * providers that any stored credential session for the given app should be cleared. + * + * @param request the request for the credential provider to handle + * @param cancellationSignal signal for observing cancellation requests. The system will + * use this to notify you that the result is no longer needed and you should stop + * handling it in order to save your resources + * @param callback the callback object to be used to notify the response or error + */ + override fun onClearCredentialStateRequest( + request: ProviderClearCredentialStateRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver, + ) { + // Delete any maintained state as appropriate. + callback.onResult(null) + } + + /** + * Creates a PendingIntent that navigates the user to the app's open/unlock flow. + * + * @param counter An AtomicInteger used to generate a unique request code for the PendingIntent. + * @param intentType The type of intent to create. + * @return A PendingIntent that can be used to launch the app's open/unlock flow. + */ + private fun createPendingIntent(counter: AtomicInteger, intentType: String): PendingIntent { + val intent = Intent(intentType).setPackage(applicationContext.packageName) + return PendingIntent.getActivity( + applicationContext, + counter.incrementAndGet(), + intent, + (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT), + ) + } + + /** + * This method helps create an action builder for opening the app. + * + * @param responseBuilder The BeginGetCredentialResponse.Builder to add the action to. + * @param counter An AtomicInteger used to generate a unique request code for the PendingIntent. + * @return True if the action was added successfully, false otherwise. + */ + private fun populateActions( + responseBuilder: BeginGetCredentialResponse.Builder, + counter: AtomicInteger, + ): Boolean { + try { + responseBuilder.addAction( + Action( + title = getString( + R.string.open, + applicationContext.getString(R.string.app_name), + ), + subtitle = getString(R.string.manage_credentials), + pendingIntent = createPendingIntent(counter, OPEN_APP_INTENT), + ), + ) + } catch (e: IOException) { + return false + } + return true + } + + companion object { + private const val OPEN_APP_INTENT = "com.example.android.authentication.myvault.OPEN_APP" + private const val UNLOCK_INTENT = "com.example.android.authentication.myvault.UNLOCK_APP" + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasskeyItem.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasskeyItem.kt new file mode 100644 index 0000000..fe355da --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasskeyItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Represents a passkey item stored in the database. + * + * @property id The unique identifier + * @property uid The user ID associated + * @property username The username associated + * @property displayName The display name + * @property credId The credential ID + * @property credPrivateKey The private key + * @property siteId The ID of the site + * @property lastUsedTimeMs The last time the passkey item was used + */ +@Entity( + tableName = "passkeys", + indices = [ + Index("credId", unique = false), + ], +) +data class PasskeyItem( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "uid") val uid: String, + @ColumnInfo(name = "username") val username: String, + @ColumnInfo(name = "displayName") val displayName: String, + @ColumnInfo(name = "credId") val credId: String, + @ColumnInfo(name = "credPrivateKey") val credPrivateKey: String, + @ColumnInfo(name = "siteId") val siteId: Long, + @ColumnInfo(name = "lastUsedTimeMs") val lastUsedTimeMs: Long, +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasswordItem.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasswordItem.kt new file mode 100644 index 0000000..b0dba67 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/PasswordItem.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Represents a password item stored in the database. + * + * @property id The unique identifier + * @property username The username + * @property password The password + * @property siteId The ID of the site + * @property lastUsedTimeMs The last time the password item was used. + */ +@Entity( + tableName = "passwords", + indices = [ + Index("username", unique = false), + ], +) +data class PasswordItem( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "username") val username: String, + @ColumnInfo(name = "password") val password: String, + @ColumnInfo(name = "siteId") val siteId: Long, + @ColumnInfo(name = "lastUsedTimeMs") val lastUsedTimeMs: Long, +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/RPIconDataSource.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/RPIconDataSource.kt new file mode 100644 index 0000000..d52b4ce --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/RPIconDataSource.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileNotFoundException +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest + +/** + * This class is responsible for providing the icons of the corresponding apps (who are saving credentials through MyVault). + * + * The RPIconDataSource class provides methods to get, save, and read rpicons from disk and the network. + */ +class RPIconDataSource(private var dataDir: String) { + + /** + * A mutable map to store the icons. + */ + private val icons: MutableMap = mutableMapOf() + + /** + * Gets the file for the given URL. + * + * @param url The URL to get the file for. + * @return The File object for the given URL. + */ + private fun getFileForUrl(url: String): File { + val hash = MessageDigest.getInstance("SHA-1").digest(url.toByteArray()) + val hashName = hash.joinToString(separator = "") { b -> "%02x".format(b) } + return File("$dataDir/rpicons", "$hashName.png") + } + + /** + * Saves the icon to disk. + * + * @param url The URL of the icon. + * @param icon The Bitmap object of the icon. + */ + private suspend fun saveToDisk(url: String, icon: Bitmap) { + return withContext(Dispatchers.IO) { + val f = getFileForUrl(url) + f.parentFile?.mkdirs() + f.createNewFile() + f.outputStream().use { + icon.compress(Bitmap.CompressFormat.PNG, 100, it) + it.flush() + } + } + } + + /** + * Gets the icon from the network. + * + * @param url The URL of the icon. + * @return The Bitmap object of the icon, or null if there was an error. + */ + private suspend fun getIconFromNetwork(url: String): Bitmap? { + return withContext(Dispatchers.IO) { + var icon: Bitmap? = null + try { + val connection = + URL("https://$url/rpicon.ico").openConnection() as HttpURLConnection + icon = BitmapFactory.decodeStream(connection.inputStream) + if (icon != null) { + saveToDisk(url, icon) + } + } catch (e: Exception) { + // Handle the exception + } + icon + } + } + + /** + * Reads the icon from disk. + * + * @param url The URL of the icon. + * @return The Bitmap object of the icon, or null if there was an error. + */ + private suspend fun readFromDisk(url: String): Bitmap? { + return withContext(Dispatchers.IO) { + val f = getFileForUrl(url) + var icon: Bitmap? = null + try { + icon = BitmapFactory.decodeStream(f.inputStream()) + } catch (e: FileNotFoundException) { + // File not found, handle error + } + icon + } + } + + /** + * Gets the icon for the provided domain URL. + * + * @param url The domain URL to get the icon for. + * @return The Bitmap object of the icon, or null if there was an error. + */ + suspend fun getIcon(url: String): Bitmap? { + if (!icons.containsKey(url)) { + val icon = readFromDisk(url) ?: getIconFromNetwork(url) + if (icon != null) { + icons[url] = icon + } + return icons[url] + } + return null + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/MyVaultDatabase.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/MyVaultDatabase.kt new file mode 100644 index 0000000..bccf7e0 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/MyVaultDatabase.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data.room + +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.Update +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.data.PasswordItem +import kotlinx.coroutines.flow.Flow + +@Database( + entities = [ + SiteMetaData::class, + PasswordItem::class, + PasskeyItem::class, + ], + version = 7, +) +abstract class MyVaultDatabase : RoomDatabase() { + abstract fun myVaultDao(): MyVaultDao +} + +@Dao +interface MyVaultDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSite(entity: SiteMetaData): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPassword(entity: PasswordItem): Long + + @Update + suspend fun updatePassword(entity: PasswordItem) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPasskey(entity: PasskeyItem): Long + + @Update + suspend fun updatePasskey(entity: PasskeyItem) + + @Delete + suspend fun deletePassword(entity: PasswordItem) + + @Delete + suspend fun deletePasskey(entity: PasskeyItem) + + @Delete + suspend fun deleteSite(entity: SiteMetaData) + + @Transaction + @Query("SELECT * FROM sites ORDER BY url") + fun siteListWithCredentials(): Flow> + + @Query("SELECT * FROM sites WHERE url = :url") + suspend fun getSite(url: String): SiteMetaData? + + @Query("SELECT COUNT(*) FROM sites WHERE url = :url") + fun getSiteCount(url: String): Int? + + @Query("SELECT COUNT(*) FROM passwords WHERE siteId = :site") + suspend fun count(site: Long): Int + + @Query("SELECT COUNT(*) FROM sites WHERE url = :site") + fun countPasskeys(site: Long): Int + + @Transaction + @Query("SELECT * FROM sites WHERE url = :url") + fun getCredentialsFromSite(url: String): SiteWithCredentials? + + @Query("SELECT * from passkeys WHERE credId = :credId") + fun getPasskey(credId: String): PasskeyItem? +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteMetaData.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteMetaData.kt new file mode 100644 index 0000000..10f995d --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteMetaData.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "sites", + indices = [ + Index("url", unique = true), + ], +) + +/** + * This class represents metadata about a site. + */ +data class SiteMetaData( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "url") val url: String = "", + @ColumnInfo(name = "packageName") val packageName: String = "", + @ColumnInfo(name = "name") val name: String = "", +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteWithCredentials.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteWithCredentials.kt new file mode 100644 index 0000000..02f8e43 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/SiteWithCredentials.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.data.room + +import androidx.room.Embedded +import androidx.room.Relation +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.data.PasswordItem + +/** + * This class represents a site with all its associated credentials, including passwords and passkeys. + */ +data class SiteWithCredentials( + @Embedded val site: SiteMetaData, + @Relation( + parentColumn = "id", + entityColumn = "siteId", + ) + val passwords: List, + + @Relation( + parentColumn = "id", + entityColumn = "siteId", + ) + val passkeys: List, +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AssetLinkVerifier.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AssetLinkVerifier.kt new file mode 100644 index 0000000..6406abd --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AssetLinkVerifier.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import android.content.pm.SigningInfo +import android.util.Log +import org.json.JSONObject +import java.net.URL +import java.security.MessageDigest + +/** + * Verifies the identity of the client app through asset linking. + * + * @param websiteUrl The domain name against which the package name and SHA needs to be verified. + */ +class AssetLinkVerifier(private val websiteUrl: String) { + + /** + * Verifies the package name and signing info for an app associated with a domain. + * + * @param callingPackage The calling package name of the calling app. + * @param callerSigningInfo The signingInfo associated with the calling app. + * @return True if the package name and signing info are valid, false otherwise. + */ + fun verify(callingPackage: String, callerSigningInfo: SigningInfo): Boolean { + val assetLinkCheckJsonResponse = callDigitalAssetLinkApi( + websiteUrl, + callingPackage, + computeLatestCertification(callerSigningInfo)!!, + ) + Log.i("AssetLinkVerifier", "Response: $assetLinkCheckJsonResponse") + return JSONObject(assetLinkCheckJsonResponse).getBoolean("linked") + } + + /** + * Computes the latest certification based on the signing info provided for a client app. + * + * @param callerSigningInfo The signingInfo associated with the calling app. + * @return The latest certification, or null if the app has multiple signers. + */ + private fun computeLatestCertification(callerSigningInfo: SigningInfo): String? { + if (callerSigningInfo.hasMultipleSigners()) { + return null + } + return computeNormalizedSha256Fingerprint( + callerSigningInfo.signingCertificateHistory[0].toByteArray(), + ) + } + + /** + * Calls the Digital Asset Link API to verify the package name and signing info. + * + * @param websiteUrl The associated domain. + * @param callingPackage The calling package name of the calling app. + * @param callingCert The latest certification computed for the calling app packagename. + * @return The response from the Digital Asset Link API. + */ + private fun callDigitalAssetLinkApi( + websiteUrl: String, + callingPackage: String, + callingCert: String, + ): String { + val apiEndpoint = "https://digitalassetlinks.googleapis.com/v1/assetlinks:check" + + "?source.web.site=$websiteUrl+" + + "&target.android_app.package_name=$callingPackage" + + "&target.android_app.certificate.sha256_fingerprint=$callingCert" + + "&relation=delegate_permission/common.handle_all_urls" + return URL(apiEndpoint).readText() + } + + /** + * Computes the normalized SHA-256 fingerprint of the given signature. + * + * @param signature The signature to compute the fingerprint for. + * @return The normalized SHA-256 fingerprint. + */ + private fun computeNormalizedSha256Fingerprint(signature: ByteArray): String { + val digest = MessageDigest.getInstance("SHA-256") + return bytesToHexString(digest.digest(signature)) + } + + /** + * Converts the given bytes to a hexadecimal string. + * + * @param bytes The bytes to convert. + * @return The hexadecimal string representation of the bytes. + */ + private fun bytesToHexString(bytes: ByteArray): String { + return bytes.joinToString(":") { "%02X".format(it) } + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAssertionResponse.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAssertionResponse.kt new file mode 100644 index 0000000..b27b577 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAssertionResponse.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import org.json.JSONObject +import java.security.MessageDigest + +/** + * The AuthenticatorAssertionResponse interface of the Web Authentication API contains a digital signature from the private key of a particular WebAuthn credential. + * + * The relying party's server can verify this signature to authenticate a user, for example when they sign in. + * + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs for AuthenticatorAssertionResponse : https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse + */ +class AuthenticatorAssertionResponse( + private val requestOptions: PublicKeyCredentialRequestOptions, + origin: String, + private val up: Boolean, + private val uv: Boolean, + private val be: Boolean, + private val bs: Boolean, + private var userHandle: ByteArray, + packageName: String? = null, + private val clientDataHash: ByteArray? = null, +) : AuthenticatorResponse { + override var clientJson = JSONObject() + private var authenticatorData: ByteArray + var signature: ByteArray = byteArrayOf() + + init { + clientJson.put("type", "webauthn.get") + clientJson.put("challenge", b64Encode(requestOptions.challenge)) + clientJson.put("origin", origin) + if (packageName != null) { + clientJson.put("androidPackageName", packageName) + } + + authenticatorData = defaultAuthenticatorData() + } + + /** + * Generates the default authenticator data. + * + * @return The default authenticator data. + */ + private fun defaultAuthenticatorData(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + val rpHash = md.digest(requestOptions.rpId.toByteArray()) + var flags = 0 + if (up) { + flags = flags or 0x01 + } + if (uv) { + flags = flags or 0x04 + } + if (be) { + flags = flags or 0x08 + } + if (bs) { + flags = flags or 0x10 + } + return rpHash + + byteArrayOf(flags.toByte()) + + byteArrayOf(0, 0, 0, 0) + } + + /** + * Computes the data to sign. + * + * @return The data to sign. + */ + fun dataToSign(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + val hash: ByteArray = clientDataHash ?: md.digest(clientJson.toString().toByteArray()) + + return authenticatorData + hash + } + + /** + * Converts the response to a JSON object. + * + * @return The JSON object representation of the response. + */ + override fun json(): JSONObject { + val clientData = clientJson.toString().toByteArray() + val response = JSONObject() + if (clientDataHash == null) { + response.put("clientDataJSON", b64Encode(clientData)) + } + response.put("authenticatorData", b64Encode(authenticatorData)) + response.put("signature", b64Encode(signature)) + response.put("userHandle", b64Encode(userHandle)) + return response + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAttestationResponse.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAttestationResponse.kt new file mode 100644 index 0000000..8e77fd4 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorAttestationResponse.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject +import java.security.MessageDigest + +/* +* The AuthenticatorAttestationResponse interface of the Web Authentication API is the result of a WebAuthn credential registration. +* It contains information about the credential that the server needs to perform WebAuthn assertions, such as its credential ID and public key. +* +* This class is used for demonstration purpose and we don't recommend you to use it directly on production. +* Please refer to standard WebAuthn specs for AuthenticatorAttestationResponse : https://www.w3.org/TR/webauthn-2/#authenticatorattestationresponse + */ +class AuthenticatorAttestationResponse( + private val requestOptions: PublicKeyCredentialCreationOptions, + private val credentialId: ByteArray, + private val credentialPublicKey: ByteArray, + origin: String, + private val up: Boolean, + private val uv: Boolean, + private val be: Boolean, + private val bs: Boolean, + packageName: String? = null, + private val clientDataHash: ByteArray? = null, + private val spki: ByteArray? = null, +) : AuthenticatorResponse { + override var clientJson = JSONObject() + private var attestationObject: ByteArray + + init { + clientJson.put("type", "webauthn.create") + clientJson.put("challenge", b64Encode(requestOptions.challenge)) + clientJson.put("origin", origin) + if (packageName != null) { + clientJson.put("androidPackageName", packageName) + } + + attestationObject = defaultAttestationObject() + } + + private fun authData(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + val rpHash = md.digest(requestOptions.rp.id.toByteArray()) + var flags = 0 + if (up) { + flags = flags or 0x01 + } + if (uv) { + flags = flags or 0x04 + } + if (be) { + flags = flags or 0x08 + } + if (bs) { + flags = flags or 0x10 + } + flags = flags or 0x40 + + val aaguid = ByteArray(16) { 0 } + val credIdLen = byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) + + return rpHash + + byteArrayOf(flags.toByte()) + + byteArrayOf(0, 0, 0, 0) + + aaguid + + credIdLen + + credentialId + + credentialPublicKey + } + + private fun addParsedAttestationObjectFieldsToJSON( + authData: ByteArray, + publicKeyAlgorithm: Long, + jsonOutput: JSONObject, + ) { + // https://www.w3.org/TR/webauthn-2/#sctn-generating-an-attestation-object + jsonOutput.put( + "authenticatorData", + b64Encode(authData), + ) + jsonOutput.put("publicKeyAlgorithm", publicKeyAlgorithm) + if (spki != null) { + jsonOutput.put("publicKey", b64Encode(spki)) + } else { + Log.i("AuthAttest", " Public key is null") + } + } + + private fun defaultAttestationObject(): ByteArray { + val ao = mutableMapOf() + ao["fmt"] = "none" + ao["attStmt"] = emptyMap() + ao["authData"] = authData() + return Cbor().encode(ao) + } + + override fun json(): JSONObject { + // See AuthenticatorAttestationResponseJSON at + // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson + + val clientData = clientJson.toString().toByteArray() + val response = JSONObject() + if (clientDataHash == null) { + response.put("clientDataJSON", b64Encode(clientData)) + } + response.put("attestationObject", b64Encode(attestationObject)) + response.put("transports", JSONArray(listOf("internal", "hybrid"))) + + addParsedAttestationObjectFieldsToJSON( + authData(), + getPublicKeyAlgorithm(), + response, + ) + + return response + } + + private fun getPublicKeyAlgorithm(): Long { + // Learn more here : https://www.iana.org/assignments/cose/cose.xhtml#algorithms + return -7 + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorResponse.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorResponse.kt new file mode 100644 index 0000000..2af65ba --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/AuthenticatorResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import org.json.JSONObject + +/** + * The AuthenticatorResponse interface of the Web Authentication API is the result of a WebAuthn credential registration. + * It contains information about the credential that the server needs to perform WebAuthn assertions, such as its credential ID and public key. + * + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs for AuthenticatorResponse : https://www.w3.org/TR/webauthn-2/#authenticatorresponse + */ +interface AuthenticatorResponse { + var clientJson: JSONObject + fun json(): JSONObject +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Cbor.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Cbor.kt new file mode 100644 index 0000000..8aceb6f --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Cbor.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import java.lang.IllegalArgumentException + +const val TYPE_UNSIGNED_INT = 0x00 +const val TYPE_NEGATIVE_INT = 0x01 +const val TYPE_BYTE_STRING = 0x02 +const val TYPE_TEXT_STRING = 0x03 +const val TYPE_ARRAY = 0x04 +const val TYPE_MAP = 0x05 + +/** + * This class helps encode the data being sent to Relying party (Rps). WebAuthn uses CBOR serialization for binary data sent to the Relying Party. + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs for Cbor : https://www.w3.org/TR/webauthn-3/#cbor + */ +class Cbor { + fun encode(data: Any): ByteArray { + if (data is Number) { + return if (data is Double) { + throw IllegalArgumentException("Don't support doubles yet") + } else { + val value = data.toLong() + if (value >= 0) { + createArg(TYPE_UNSIGNED_INT, value) + } else { + createArg(TYPE_NEGATIVE_INT, -1 - value) + } + } + } + if (data is ByteArray) { + return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data + } + if (data is String) { + return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray() + } + if (data is List<*>) { + var ret = createArg(TYPE_ARRAY, data.size.toLong()) + for (i in data) { + ret += encode(i!!) + } + return ret + } + if (data is Map<*, *>) { + // Refer here: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form + var ret = createArg(TYPE_MAP, data.size.toLong()) + for (i in data) { + ret += encode(i.key!!) + ret += encode(i.value!!) + } + return ret + } + throw IllegalArgumentException("Bad type") + } + + private fun createArg(type: Int, arg: Long): ByteArray { + val t = type shl 5 + val a = arg.toInt() + if (arg < 24) { + return byteArrayOf(((t or a) and 0xFF).toByte()) + } + if (arg <= 0xFF) { + return byteArrayOf( + ((t or 24) and 0xFF).toByte(), + (a and 0xFF).toByte(), + ) + } + if (arg <= 0xFFFF) { + return byteArrayOf( + ((t or 25) and 0xFF).toByte(), + ((a shr 8) and 0xFF).toByte(), + (a and 0xFF).toByte(), + ) + } + if (arg <= 0xFFFFFFFF) { + return byteArrayOf( + ((t or 26) and 0xFF).toByte(), + ((a shr 24) and 0xFF).toByte(), + ((a shr 16) and 0xFF).toByte(), + ((a shr 8) and 0xFF).toByte(), + (a and 0xFF).toByte(), + ) + } + throw IllegalArgumentException("bad Arg") + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoDataTypes.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoDataTypes.kt new file mode 100644 index 0000000..12890a0 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoDataTypes.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +/** + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs + */ +data class PublicKeyCredentialRpEntity( + val name: String, + val id: String, +) + +data class PublicKeyCredentialUserEntity( + val name: String, + val id: ByteArray, + val displayName: String, +) + +data class PublicKeyCredentialParameters( + val type: String, + val alg: Long, +) + +data class PublicKeyCredentialDescriptor( + val type: String, + val id: ByteArray, + val transports: List, +) + +data class AuthenticatorSelectionCriteria( + val authenticatorAttachment: String, + val residentKey: String, + val requireResidentKey: Boolean = false, + val userVerification: String = "preferred", +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoPublicKeyCredential.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoPublicKeyCredential.kt new file mode 100644 index 0000000..1e76ac6 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/FidoPublicKeyCredential.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import org.json.JSONObject + +/** + * The PublicKeyCredential interface inherits from Credential, and contains the attributes that are returned to the caller when a new credential is created, or a new assertion is requested. + * + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs : https://www.w3.org/TR/webauthn-2/#iface-pkcredential + */ +class FidoPublicKeyCredential( + val rawId: ByteArray, + val response: AuthenticatorResponse, + val authenticatorAttachment: String, +) { + + fun json(): String { + // See RegistrationResponseJSON at + // https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson + // https://www.w3.org/TR/webauthn-2/#publickeycredential + + val encodedId = b64Encode(rawId) + val ret = JSONObject() + ret.put("id", encodedId) + ret.put("rawId", encodedId) + ret.put("type", "public-key") + ret.put("authenticatorAttachment", authenticatorAttachment) + ret.put("response", response.json()) + ret.put("clientExtensionResults", extensionJson()) + return ret.toString() + } + + private fun extensionJson(): JSONObject { + val json = JSONObject() + json.put("credProps", credPropsJson()) + return json + } + + private fun credPropsJson(): JSONObject { + val response = JSONObject() + response.put("rk", true) + return response + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialCreationOptions.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialCreationOptions.kt new file mode 100644 index 0000000..8fc3865 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialCreationOptions.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import org.json.JSONObject + +/** + * The PublicKeyCredentialCreationOptions dictionary of the Web Authentication API holds options passed to client app's createCredential() call in order to create a PublicKeyCredential. + * + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs : https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions + */ +class PublicKeyCredentialCreationOptions(requestJson: String) { + private val json: JSONObject + + val rp: PublicKeyCredentialRpEntity + val user: PublicKeyCredentialUserEntity + val challenge: ByteArray + private val pubKeyCredParams: List + + private var timeout: Long + private var excludeCredentials: List + private var authenticatorSelection: AuthenticatorSelectionCriteria + private var attestation: String + + init { + + json = JSONObject(requestJson) + val challengeString = json.getString("challenge") + challenge = b64Decode(challengeString) + val rpJson = json.getJSONObject("rp") + rp = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id")) + val rpUser = json.getJSONObject("user") + val userId = b64Decode(rpUser.getString("id")) + user = PublicKeyCredentialUserEntity( + rpUser.getString("name"), userId, rpUser.getString("displayName"), + ) + val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams") + val pubKeyCredParamsTmp: MutableList = mutableListOf() + for (i in 0 until pubKeyCredParamsJson.length()) { + val e = pubKeyCredParamsJson.getJSONObject(i) + pubKeyCredParamsTmp.add( + PublicKeyCredentialParameters( + e.getString("type"), + e.getLong("alg"), + ), + ) + } + pubKeyCredParams = pubKeyCredParamsTmp.toList() + + timeout = json.optLong("timeout", 0) + + excludeCredentials = emptyList() + authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required") + attestation = json.optString("attestation", "none") + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialRequestOptions.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialRequestOptions.kt new file mode 100644 index 0000000..0fd572f --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/PublicKeyCredentialRequestOptions.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import org.json.JSONObject + +/** + * The PublicKeyCredentialRequestOptions dictionary of the Web Authentication API holds the options passed to client app's getCredential() call in order to fetch a given PublicKeyCredential. + * + * This class is used for demonstration purpose and we don't recommend you to use it directly on production. + * Please refer to standard WebAuthn specs : https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options + */ +class PublicKeyCredentialRequestOptions(requestJson: String) { + private val json: JSONObject + + val challenge: ByteArray + private val timeout: Long + val rpId: String + private val userVerification: String + + init { + json = JSONObject(requestJson) + + val challengeString = json.getString("challenge") + challenge = b64Decode(challengeString) + timeout = json.optLong("timeout", 0) + rpId = json.optString("rpId", "") + userVerification = json.optString("userVerification", "preferred") + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Util.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Util.kt new file mode 100644 index 0000000..9609706 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/fido/Util.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.fido + +import android.util.Base64 +import androidx.credentials.provider.CallingAppInfo +import java.security.MessageDigest + +/** + * Decodes a Base64-encoded string into a byte array. + * + * @param str The Base64-encoded string. + * @return The decoded byte array. + */ +fun b64Decode(str: String): ByteArray { + return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE) +} + +/** + * Encodes a byte array into a Base64-encoded string. + * + * @param data The byte array to encode. + * @return The Base64-encoded string. + */ +fun b64Encode(data: ByteArray): String { + return Base64.encodeToString(data, Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE) +} + +/** + * Generates an origin string for a given CallingAppInfo object. + * + * @param info The CallingAppInfo object. + * @return The origin string. + * refer the origin documentation for explanation : https://developer.android.com/training/sign-in/passkeys#verify-origin + * + */ +fun appInfoToOrigin(info: CallingAppInfo): String { + val cert = info.signingInfo.apkContentsSigners[0].toByteArray() + val md = MessageDigest.getInstance("SHA-256") + val certHash = md.digest(cert) + return "android:apk-key-hash:${b64Encode(certHash)}" +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/AppDrawer.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/AppDrawer.kt new file mode 100644 index 0000000..a0a9870 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/AppDrawer.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.android.authentication.myvault.Dimensions +import com.example.android.authentication.myvault.R + +/** + * The AppDrawer composable function creates a drawer for the MyVault app. + * + * @param currentRoute The current route of the app. + * @param navigateToHome A lambda function to navigate to the home screen. + * @param navigateToSettings A lambda function to navigate to the settings screen. + * @param closeDrawer A lambda function to close the drawer. + */ +@Composable +fun AppDrawer( + currentRoute: String, + navigateToHome: () -> Unit, + navigateToSettings: () -> Unit, + closeDrawer: () -> Unit, +) { + MyVaultContent() + HorizontalDivider(color = MaterialTheme.colorScheme.primary.copy(alpha = .2f)) + DrawerButton( + image = Icons.Filled.Home, + label = (stringResource(R.string.credentials)), + isSelected = currentRoute == MyVaultDestinations.HOME_ROUTE, + action = { + navigateToHome() + closeDrawer() + }, + ) + DrawerButton( + image = Icons.Filled.Settings, + label = (stringResource(R.string.settings)), + isSelected = currentRoute == MyVaultDestinations.SETTINGS_ROUTE, + action = { + navigateToSettings() + closeDrawer() + }, + ) +} + +/** + * The MyVaultContent composable function displays the MyVault logo and app name. + * + * @param modifier The modifier to be applied to the composable. + */ +@Composable +private fun MyVaultContent(modifier: Modifier = Modifier) { + Row( + modifier.padding(top = Dimensions.padding_medium, bottom = Dimensions.padding_medium), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier.width(Dimensions.padding_large)) + Icon( + painter = painterResource(R.drawable.android_secure), + contentDescription = stringResource(id = R.string.app_name), + tint = MaterialTheme.colorScheme.primary, + modifier = modifier.size(40.dp), + ) + Text( + color = MaterialTheme.colorScheme.primary, + text = "MyVault", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge, + modifier = modifier.align(Alignment.CenterVertically), + ) + } +} + +/** + * The DrawerButton composable function creates a button for the MyVault app's navigation drawer. + * + * @param image The image to display on the button. + * @param label The text to display on the button. + * @param isSelected Whether the button is currently selected. + * @param action The action to perform when the button is clicked. + * @param modifier The modifier to be applied to the composable. + */ +@Composable +private fun DrawerButton( + image: ImageVector, + label: String, + isSelected: Boolean, + action: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = NavigationDrawerItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.background, + selectedTextColor = MaterialTheme.colorScheme.background, + unselectedIconColor = MaterialTheme.colorScheme.primary, + unselectedTextColor = MaterialTheme.colorScheme.primary, + selectedContainerColor = MaterialTheme.colorScheme.primary, + unselectedContainerColor = MaterialTheme.colorScheme.background, + ) + + Spacer(modifier.width(Dimensions.padding_large)) + NavigationDrawerItem( + onClick = action, + icon = { Icon(image, null) }, + label = { + Text( + label, + style = MaterialTheme.typography.bodyMedium, + ) + }, + selected = isSelected, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 1.dp, + top = Dimensions.padding_medium, + end = 1.dp, + ), + colors = colors, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasskeyActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasskeyActivity.kt new file mode 100644 index 0000000..c1a7aea --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasskeyActivity.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.app.Activity +import android.content.Intent +import android.content.pm.SigningInfo +import android.os.Bundle +import android.util.Base64 +import android.util.Log +import androidx.activity.enableEdgeToEdge +import androidx.biometric.BiometricManager.Authenticators +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo.Builder +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.provider.CallingAppInfo +import androidx.credentials.provider.PendingIntentHandler +import androidx.credentials.provider.ProviderCreateCredentialRequest +import androidx.fragment.app.FragmentActivity +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.PasskeyMetadata +import com.example.android.authentication.myvault.fido.AssetLinkVerifier +import com.example.android.authentication.myvault.fido.AuthenticatorAttestationResponse +import com.example.android.authentication.myvault.fido.Cbor +import com.example.android.authentication.myvault.fido.FidoPublicKeyCredential +import com.example.android.authentication.myvault.fido.PublicKeyCredentialCreationOptions +import com.example.android.authentication.myvault.fido.appInfoToOrigin +import com.example.android.authentication.myvault.fido.b64Encode +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import java.math.BigInteger +import java.net.URL +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.SecureRandom +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECGenParameterSpec +import java.time.Instant + +/** + * This class is responsible for handling the public key credential (Passkey) creation request from a Relying Party i.e calling app + */ +class CreatePasskeyActivity : FragmentActivity() { + private val credentialsDataSource = AppDependencies.credentialsDataSource + + companion object { + private const val INVALID_ALLOWLIST = "{\"apps\": [\n" + + " {\n" + + " \"type\": \"android\", \n" + + " \"info\": {\n" + + " \"package_name\": \"androidx.credentials.test\",\n" + + " \"signatures\" : [\n" + + " {\"build\": \"release\",\n" + + " \"cert_fingerprint_sha256\": \"HELLO\"\n" + + " },\n" + + " {\"build\": \"ud\",\n" + + " \"cert_fingerprint_sha256\": \"YELLOW\"\n" + + " }]\n" + + " }\n" + + " }\n" + + "]}\n" + + "\n" + private const val GPM_ALLOWLIST_URL = + "https://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json" + + private const val TAG = "MyVault" + const val KEY_ACCOUNT_LAST_USED_MS = "key_account_last_used_ms" + const val KEY_ACCOUNT_ID = "key_account_id" + const val USER_ACCOUNT = "user_account" + } + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) + + if (request == null) { + // If the request is null, send an unknown exception to client and finish the flow + setUpFailureResponseAndFinish("Unable to extract request from intent") + return + } + + handleCreatePublicKeyCredentialRequest(request) + } + + /** + * If the request is null, send an unknown exception to client and finish the flow. + * + * @param message The error message to send to the client. + */ + private fun setUpFailureResponseAndFinish(message: String) { + val result = Intent() + PendingIntentHandler.setGetCredentialException( + result, + GetCredentialUnknownException(message), + ) + setResult(Activity.RESULT_OK, result) + finish() + } + + /** + * This method handle public key credential creation request from client app + * + * @param request : Final request received by the provider after the user has selected a given CreateEntry on the UI. + */ + private fun handleCreatePublicKeyCredentialRequest(request: ProviderCreateCredentialRequest) { + val accountId = intent.getStringExtra(KEY_ACCOUNT_ID) + + // access the associated intent and pass it into the PendingIntentHandler class to get the ProviderCreateCredentialRequest. + if (request.callingRequest is CreatePublicKeyCredentialRequest) { + val publicKeyRequest: CreatePublicKeyCredentialRequest = + request.callingRequest as CreatePublicKeyCredentialRequest + createPasskey( + publicKeyRequest.requestJson, + request.callingAppInfo, + publicKeyRequest.clientDataHash, + accountId, + ) + } else { + setUpFailureResponseAndFinish("Unexpected create request found in intent") + return + } + } + + /** + * This method validates the digital asset linking to verify the app identity, + * surface biometric prompt and sends back response to client app for passkey created + * + * @param requestJson the request in JSON format in the [standard webauthn web json](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). + * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing + * @param clientDataJSON during the signature request; only meaningful when [origin] is set + * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing + */ + private fun createPasskey( + requestJson: String, + callingAppInfo: CallingAppInfo?, + clientDataHash: ByteArray?, + accountId: String?, + ) { + if (callingAppInfo == null) { + finish() + return + } + + val request = PublicKeyCredentialCreationOptions(requestJson) + + var callingAppInfoOrigin: String? = null + if (hasRequestContainsOrigin(callingAppInfo)) { + callingAppInfoOrigin = validatePrivilegedCallingApp( + callingAppInfo, + ) ?: return + } else { + // Native call. Check for asset links + validateAssetLinks(request.rp.id, callingAppInfo) + } + + // Surface an authentication prompt. The example below uses the Android Biometric API. + val biometricPrompt = BiometricPrompt( + this, + mainExecutor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, getString(R.string.authentication_error, errString)) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.e(TAG, getString(R.string.authentication_failed)) + finish() + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult, + ) { + super.onAuthenticationSucceeded(result) + + // Generate CredentialID + val credentialId = ByteArray(32) + SecureRandom().nextBytes(credentialId) + + // Generate key + val keyPair = generateKeyPair() + + // Save the private key in your local database against callingAppInfo.packageName. + savePasskeyInCredentialsDataStore(request, credentialId, keyPair) + + updateMetaInSharedPreferences(accountId) + + var callingOrigin = appInfoToOrigin(callingAppInfo) + if (callingAppInfoOrigin != null) { + callingOrigin = callingAppInfoOrigin + } + val response = constructWebAuthnResponse( + keyPair, + request, + credentialId, + callingOrigin, + callingAppInfo, + clientDataHash, + ) + + setIntentForCredentialCredentialResponse(credentialId, response) + } + }, + ) + authenticate(biometricPrompt) + } + + /** + * This method constructs a WebAuthn response object. + * + * @param keyPair The key pair used to generate the credential. + * @param request The PublicKeyCredentialCreationOptions object containing the client's request. + * @param credId The credential ID. + * @param callingOrigin The origin of the calling application. + * @param callingAppInfo Information about the calling application. + * @param clientDataHash The client data hash. + * @return An AuthenticatorAttestationResponse object. + */ + private fun constructWebAuthnResponse( + keyPair: KeyPair, + request: PublicKeyCredentialCreationOptions, + credId: ByteArray, + callingOrigin: String, + callingAppInfo: CallingAppInfo, + clientDataHash: ByteArray?, + ): AuthenticatorAttestationResponse { + val coseKey = publicKeyToCose(keyPair.public as ECPublicKey) + val spki = coseKeyToSPKI(coseKey) + + // Construct a Web Authentication API JSON response that consists of the public key and the credentialId. + val response = AuthenticatorAttestationResponse( + requestOptions = request, + credentialId = credId, + credentialPublicKey = Cbor().encode(coseKey), + origin = callingOrigin, + up = true, + uv = true, + be = true, + bs = true, + packageName = callingAppInfo.packageName, + clientDataHash = clientDataHash, + spki, + ) + return response + } + + /** + * Generates a new key pair for use in creating a public key credential. + * + * @return A new [KeyPair] instance. + */ + private fun generateKeyPair(): KeyPair { + val spec = ECGenParameterSpec(getString(R.string.secp_256_r1)) + val keyPairGen = KeyPairGenerator.getInstance(getString(R.string.ec)) + keyPairGen.initialize(spec) + return keyPairGen.genKeyPair() + } + + /** + * This method helps check the asset linking to verify client app idenity + * @param rpId : Relying party identifier + * @param callingAppInfo : Information pertaining to the calling application. + */ + private fun validateAssetLinks(rpId: String, callingAppInfo: CallingAppInfo) { + val isRpValid: Boolean = runBlocking { + val isRpValidDeferred: Deferred = async(Dispatchers.IO) { + if (!isValidRpId( + rpId, + callingAppInfo.signingInfo, + callingAppInfo.packageName, + ) + ) { + return@async false + } + return@async true + } + return@runBlocking isRpValidDeferred.await() + } + + if (!isRpValid) { + setUpFailureResponseAndFinish("Failed to validate rp") + return + } + } + + /** + * Checks if the given Relying Party (RP) identifier is valid. + * + * @param rpId The RP identifier to validate. + * @param signingInfo The signing information of the calling application. + * @param callingPackage The package name of the calling application. + * @return True if the RP identifier is valid, false otherwise. + */ + private fun isValidRpId( + rpId: String, + signingInfo: SigningInfo, + callingPackage: String, + ): Boolean { + val websiteUrl = "https://$rpId" + val assetLinkVerifier = AssetLinkVerifier(websiteUrl) + try { + // log the info returned. + return assetLinkVerifier.verify(callingPackage, signingInfo) + } catch (e: Exception) { + // Log exception + } + return false + } + + /** + * This method helps check if the app called is allowlisted through Google Password Manager + * @param callingAppInfo : Information pertaining to the calling application. + */ + private fun validatePrivilegedCallingApp(callingAppInfo: CallingAppInfo): String? { + val privilegedAppsAllowlist = getGPMPrivilegedAppAllowlist() + if (privilegedAppsAllowlist != null) { + return try { + callingAppInfo.getOrigin( + privilegedAppsAllowlist, + ) + } catch (e: IllegalStateException) { + val message = "Incoming call is not privileged to get the origin" + setUpFailureResponseAndFinish(message) + null + } catch (e: IllegalArgumentException) { + val message = "Privileged allowlist is not formatted properly" + setUpFailureResponseAndFinish(message) + null + } + } + val message = "Could not retrieve GPM allowlist" + setUpFailureResponseAndFinish(message) + return null + } + + /** + * Method to retrieve the list of privileged apps allowlisted by GPM + */ + private fun getGPMPrivilegedAppAllowlist(): String? { + val gpmAllowlist: String? = runBlocking { + val allowlist: Deferred = async(Dispatchers.IO) { + try { + val url = URL(GPM_ALLOWLIST_URL) + return@async url.readText() + } catch (e: Exception) { + return@async null + } + } + return@runBlocking allowlist.await() + } + + return gpmAllowlist + } + + /** + * Checks if the client request contains an origin. + * + * @param callingAppInfo Information pertaining to the calling application. + * @return True if the request contains an origin, false otherwise. + */ + private fun hasRequestContainsOrigin(callingAppInfo: CallingAppInfo): Boolean { + try { + callingAppInfo.getOrigin(INVALID_ALLOWLIST) + } catch (e: IllegalStateException) { + return true + } + return false + } + + /** + * Converts a COSE key to an SPKI (Subject Public Key Info) byte array. + * + * @param coseKey A mutable map representing the COSE key. + * @return The SPKI byte array, or null if an error occurs. + */ + private fun coseKeyToSPKI(coseKey: MutableMap): ByteArray? { + try { + val spkiPrefix: ByteArray = Base64.decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE", 0) + val x = coseKey[-2] as ByteArray + val y = coseKey[-3] as ByteArray + return spkiPrefix + x + y + } catch (e: Exception) { + // Log exceptions + } + return null + } + + /** + * Set intent response to send back to the calling Relying party/client app + * @param credentialId : generated credential ID + * @param response : response to be sent back to calling app + */ + private fun setIntentForCredentialCredentialResponse( + credentialId: ByteArray, + response: AuthenticatorAttestationResponse, + ) { + val credential = FidoPublicKeyCredential( + rawId = credentialId, + response = response, + authenticatorAttachment = getString(R.string.platform), + ) + val intent = Intent() + // Construct a CreatePublicKeyCredentialResponse with the JSON generated above. + val publicKeyResponse = CreatePublicKeyCredentialResponse(credential.json()) + + // Set CreatePublicKeyCredentialResponse as an extra on an Intent through PendingIntentHandler.setCreateCredentialResponse(), + // and set that intent to the result of the Activity. + PendingIntentHandler.setCreateCredentialResponse(intent, publicKeyResponse) + setResult(RESULT_OK, intent) + finish() + } + + /** + * Surfaces the biometric prompt to use the screen lock. + * + * @param biometricPrompt The biometric prompt to use. + */ + private fun authenticate( + biometricPrompt: BiometricPrompt, + ) { + val promptInfo = Builder() + .setTitle(getString(R.string.use_your_screen_lock)) + .setSubtitle(getString(R.string.use_fingerprint)) + .setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL) + .build() + biometricPrompt.authenticate(promptInfo) + } + + /** + * Converts an ECPublicKey to a COSE key. + * + * @param key The ECPublicKey to convert. + * @return A mutable map representing the COSE key. + */ + private fun publicKeyToCose(key: ECPublicKey): MutableMap { + val x = bigIntToFixedArray(key.w.affineX) + val y = bigIntToFixedArray(key.w.affineY) + val coseKey = mutableMapOf() + coseKey[1] = 2 // EC Key type + coseKey[3] = -7 // ES256 + coseKey[-1] = 1 // P-265 Curve + coseKey[-2] = x // x + coseKey[-3] = y // y + return coseKey + } + + private fun bigIntToFixedArray(n: BigInteger): ByteArray { + assert(n.signum() >= 0) + + val bytes = n.toByteArray() + // `toByteArray` will left-pad with a leading zero if the + // most-significant bit of the first byte would otherwise be one. + var offset = 0 + if (bytes[0] == 0x00.toByte()) { + offset++ + } + val bytesLen = bytes.size - offset + assert(bytesLen <= 32) + + val output = ByteArray(32) + System.arraycopy(bytes, offset, output, 32 - bytesLen, bytesLen) + return output + } + + /** + * Updates the metadata in shared preferences. + * + * @param accountId The account ID. + */ + private fun updateMetaInSharedPreferences(accountId: String?) { + if (accountId == null || (accountId != USER_ACCOUNT)) { + // AccountId was not set + } else { + applicationContext.getSharedPreferences( + applicationContext.packageName, + MODE_PRIVATE, + ).edit().apply { + putLong( + KEY_ACCOUNT_LAST_USED_MS, + Instant.now().toEpochMilli(), + ) + }.apply() + } + } + + /** + * Saves the passkey in the credentials data store. + * + * @param request The public key credential creation options. + * @param credId The credential ID. + * @param keyPair The key pair. + */ + private fun savePasskeyInCredentialsDataStore( + request: PublicKeyCredentialCreationOptions, + credId: ByteArray, + keyPair: KeyPair, + ) { + runBlocking { + credentialsDataSource.addNewPasskey( + PasskeyMetadata( + uid = b64Encode(request.user.id), + rpid = request.rp.id, + username = request.user.name, + displayName = request.user.displayName, + credId = b64Encode(credId), + credPrivateKey = b64Encode((keyPair.private as ECPrivateKey).s.toByteArray()), + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasswordActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasswordActivity.kt new file mode 100644 index 0000000..3e74085 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/CreatePasswordActivity.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.rememberCoroutineScope +import androidx.credentials.CreatePasswordRequest +import androidx.credentials.CreatePasswordResponse +import androidx.credentials.provider.PendingIntentHandler +import androidx.credentials.provider.ProviderCreateCredentialRequest +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.data.PasswordMetaData +import com.example.android.authentication.myvault.ui.password.PasswordScreen +import kotlinx.coroutines.launch +import java.time.Instant + +/* +* This class is responsible for handling the password credential (Passkey) creation request from a Relying Party i.e calling app + */ +class CreatePasswordActivity : ComponentActivity() { + private val credentialsDataSource = AppDependencies.credentialsDataSource + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + // Access the associated intent and pass it into the PendingIntentHander class to get the ProviderCreateCredentialRequest method. + val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) + val accountId = intent.getStringExtra(KEY_ACCOUNT_ID) + + handlePassword(createRequest, accountId) + } + + /** + * This method handles the password creation request + * @param : createRequest : Final request received by the provider after the user has selected a given CreateEntry on the UI. + * @param accountId : user's unique account id + */ + private fun handlePassword( + createRequest: ProviderCreateCredentialRequest?, + accountId: String?, + ) { + if (createRequest != null) { + if (createRequest.callingRequest is CreatePasswordRequest) { + val request: CreatePasswordRequest = + createRequest.callingRequest as CreatePasswordRequest + + setContent { + val coroutineScope = rememberCoroutineScope() + PasswordScreen( + onSave = { + coroutineScope.launch { + onSaveClick(request, createRequest, accountId) + } + }, + ) + } + } + } + } + + /** + * Saves the password and sets the response back to the calling app. + * + * @param request The create password request. + * @param createRequest The provider create credential request. + * @param accountId The user's unique account ID. + */ + private suspend fun onSaveClick( + request: CreatePasswordRequest, + createRequest: ProviderCreateCredentialRequest, + accountId: String?, + ) { + savePassword( + request.id, + request.password, + createRequest.callingAppInfo.packageName, + accountId, + ) + // Set the response back + val result = Intent() + val response = CreatePasswordResponse() + PendingIntentHandler.setCreateCredentialResponse(result, response) + setResult(RESULT_OK, result) + this.finish() + } + + /** + * Saves the user password in storage. + * + * @param username The username. + * @param password The password. + * @param callingPackage The package name of the calling app. + * @param accountId The user's unique account ID. + */ + private suspend fun savePassword( + username: String, + password: String, + callingPackage: String?, + accountId: String?, + ) { + if (callingPackage == null) { + return + } + + if (accountId == null || (accountId != USER_ACCOUNT)) { + // AccountId was not set + } else { + saveUserPassword() + } + + credentialsDataSource.addNewPassword( + PasswordMetaData( + username, + password, + callingPackage, + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + + /** + * Saves the user password in storage. + */ + private fun saveUserPassword() { + applicationContext.getSharedPreferences( + applicationContext.packageName, + MODE_PRIVATE, + ).edit().apply { + putLong( + KEY_ACCOUNT_LAST_USED_MS, + Instant.now().toEpochMilli(), + ) + }.apply() + } + + companion object { + const val KEY_ACCOUNT_LAST_USED_MS = "key_account_last_used_ms" + const val KEY_ACCOUNT_ID = "key_account_id" + const val USER_ACCOUNT = "user_account" + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasskeyActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasskeyActivity.kt new file mode 100644 index 0000000..aec0e5e --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasskeyActivity.kt @@ -0,0 +1,503 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.app.Activity +import android.content.Intent +import android.content.pm.SigningInfo +import android.os.Bundle +import android.util.Log +import androidx.activity.enableEdgeToEdge +import androidx.biometric.BiometricManager.Authenticators +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationCallback +import androidx.biometric.BiometricPrompt.AuthenticationResult +import androidx.biometric.BiometricPrompt.PromptInfo.Builder +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.provider.CallingAppInfo +import androidx.credentials.provider.PendingIntentHandler +import androidx.fragment.app.FragmentActivity +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.fido.AssetLinkVerifier +import com.example.android.authentication.myvault.fido.AuthenticatorAssertionResponse +import com.example.android.authentication.myvault.fido.FidoPublicKeyCredential +import com.example.android.authentication.myvault.fido.PublicKeyCredentialRequestOptions +import com.example.android.authentication.myvault.fido.appInfoToOrigin +import com.example.android.authentication.myvault.fido.b64Decode +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import java.math.BigInteger +import java.net.URL +import java.security.AlgorithmParameters +import java.security.KeyFactory +import java.security.Signature +import java.security.interfaces.ECPrivateKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECParameterSpec +import java.security.spec.ECPrivateKeySpec +import java.time.Instant + +/* +* This class is responsible for handling the public key credential (Passkey) get request from a Relying Party i.e calling app + */ +class GetPasskeyActivity : FragmentActivity() { + + private val credentialsDataSource = AppDependencies.credentialsDataSource + + public override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + handleGetPasskeyIntent() + } + + /** + * Handle the intent for get public key credential (passkey) + */ + private fun handleGetPasskeyIntent() { + val requestInfo = intent.getBundleExtra(getString(R.string.vault_data)) + intent.getBooleanExtra(getString(R.string.is_auto_selected), false) + + /* + * retrieveProviderGetCredentialRequest extracts the [ProviderGetCredentialRequest] from the provider's + * [PendingIntent] invoked by the Android system, when the user selects a + * [CredentialEntry]. + */ + val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) + + if (requestInfo == null || request == null) { + setUpFailureResponseAndFinish(getString(R.string.unable_to_retrieve_data_from_intent)) + return + } + + val req = request.credentialOptions[0] + + // Extract the GetPublicKeyCredentialOption from the request retrieved above. + val publicKeyRequest = req as GetPublicKeyCredentialOption + val publicKeyRequestOptions = PublicKeyCredentialRequestOptions( + publicKeyRequest.requestJson, + ) + var callingAppOriginInfo: String? = null + + if (hasRequestContainsOrigin(request.callingAppInfo)) { + callingAppOriginInfo = validatePrivilegedCallingApp( + request.callingAppInfo, + ) + } else { + // Native call. Check for asset links to verify app's identity + validateAssetLinks( + publicKeyRequestOptions.rpId, + request.callingAppInfo, + ) + } + + // Extract the requestJson and clientDataHash from this option. + var clientDataHash: ByteArray? = null + if (request.callingAppInfo.origin != null) { + clientDataHash = publicKeyRequest.clientDataHash + } + + if (callingAppOriginInfo != null) { + clientDataHash = publicKeyRequest.clientDataHash + } + + assertPasskey( + request.callingAppInfo, + clientDataHash, + requestInfo, + publicKeyRequest.requestJson, + callingAppOriginInfo, + ) + } + + /** + * This method helps check the asset linking to verify client app idenity + * @param rpId : Relying party identifier + * @param callingAppInfo : Information pertaining to the calling application. + */ + private fun validateAssetLinks(rpId: String, callingAppInfo: CallingAppInfo) { + val isRpValid: Boolean = runBlocking { + val isRpValidDeferred: Deferred = async(Dispatchers.IO) { + return@async isValidRpId( + rpId, + callingAppInfo.signingInfo, + callingAppInfo.packageName, + ) + } + return@runBlocking isRpValidDeferred.await() + } + + if (!isRpValid) { + setUpFailureResponseAndFinish("Failed to validate rp") + return + } + } + + /** + * Validates the Relying Party (RP) identifier using asset linking. + * + * @param rpId The identifier of the RP. + * @param signingInfo The signing information of the calling app. + * @param callingPackage The package name of the calling app. + * @return True if the RP is valid, false otherwise. + */ + private fun isValidRpId( + rpId: String, + signingInfo: SigningInfo, + callingPackage: String, + ): Boolean { + val websiteUrl = "https://$rpId" + val assetLinkVerifier = AssetLinkVerifier(websiteUrl) + try { + return assetLinkVerifier.verify(callingPackage, signingInfo) + } catch (e: Exception) { + // Log exception + } + return false + } + + /** + * Validates if the app is privileged to get the origin, i.e., allowlisted in GPM privileged apps. + * + * @param callingAppInfo Information pertaining to the calling application. + * @return The origin if the app is privileged, or null otherwise. + */ + private fun validatePrivilegedCallingApp(callingAppInfo: CallingAppInfo): String? { + val privAppAllowlistJson = getGPMPrivilegedAppAllowlist() + if (privAppAllowlistJson != null) { + return try { + callingAppInfo.getOrigin( + privAppAllowlistJson, + ) + } catch (e: IllegalStateException) { + val message = "Incoming call is not privileged to get the origin" + setUpFailureResponseAndFinish(message) + null + } catch (e: IllegalArgumentException) { + val message = "Privileged allowlist is not formatted properly" + setUpFailureResponseAndFinish(message) + null + } + } + val message = "Could not retrieve GPM allowlist" + setUpFailureResponseAndFinish(message) + return null + } + + /** + * Method to retrieve the list of privileged apps allowlisted by GPM + * + * @return The allowlist as a JSON string, or null if there is an error. + */ + private fun getGPMPrivilegedAppAllowlist(): String? { + val gpmAllowlist: String? = runBlocking { + val allowlist: Deferred = async(Dispatchers.IO) { + try { + val url = URL(GPM_ALLOWLIST_URL) + return@async url.readText() + } catch (e: Exception) { + return@async null + } + } + return@runBlocking allowlist.await() + } + + return gpmAllowlist + } + + /** + * Checks if the client request contains an origin for the calling app. + * + * @param callingAppInfo Information pertaining to the calling application. + * @return True if the request contains an origin, false otherwise. + */ + private fun hasRequestContainsOrigin(callingAppInfo: CallingAppInfo): Boolean { + try { + callingAppInfo.getOrigin(INVALID_ALLOWLIST) + } catch (e: IllegalStateException) { + return true + } catch (e: IllegalArgumentException) { + return false + } + return false + } + + /** + * Sets up a failure response and finishes the activity. + * + * @param message The error message to include in the response. + */ + private fun setUpFailureResponseAndFinish(message: String) { + val result = Intent() + PendingIntentHandler.setGetCredentialException( + result, + GetCredentialUnknownException(message), + ) + setResult(Activity.RESULT_OK, result) + finish() + } + + /** + * Confirm that the passkey is valid with extracted metadata, and user verification. + * + * @param callingAppInfo Information pertaining to the calling application. + * @param clientDataHash a clientDataHash value to sign over in place of assembling and hashing + * @param requestInfo type of information requested + * @param requestJson calling app metadata + * @param callingAppOriginInfo information if app is priviliged to get the origin + * + */ + private fun assertPasskey( + callingAppInfo: CallingAppInfo, + clientDataHash: ByteArray?, + requestInfo: Bundle, + requestJson: String, + callingAppOriginInfo: String?, + ) { + val credIdEnc = requestInfo.getString(getString(R.string.cred_id))!! + val passkey = credentialsDataSource.getPasskey(credIdEnc)!! + + val credId = b64Decode(credIdEnc) + val privateKey = b64Decode(passkey.credPrivateKey) + val uid = b64Decode(passkey.uid) + + val origin = appInfoToOrigin(callingAppInfo) + val packageName = callingAppInfo.packageName + + val request = PublicKeyCredentialRequestOptions(requestJson) + val convertedPrivateKey = convertPrivateKey(privateKey) + + val biometricPrompt = configureBioMetricPrompt( + passkey, + origin, + callingAppOriginInfo, + request, + uid, + packageName, + clientDataHash, + convertedPrivateKey, + credId, + ) + + authenticate(biometricPrompt) + } + + /** + * Configures the BiometricPrompt with authentication callbacks. + * + * @param passkey The PasskeyItem associated with the authentication. + * @param origin The origin of the calling app. + * @param callingAppInfoOrigin The origin of the calling app if it is privileged. + * @param request The PublicKeyCredentialRequestOptions for the authentication. + * @param uid The user ID for the authentication. + * @param packageName The package name of the calling app. + * @param clientDataHash The client data hash for the authentication. + * @param convertedPrivateKey The converted private key for the authentication. + * @param credId The credential ID for the authentication. + * + * @return The configured BiometricPrompt. + */ + private fun configureBioMetricPrompt( + passkey: PasskeyItem, + origin: String, + callingAppInfoOrigin: String?, + request: PublicKeyCredentialRequestOptions, + uid: ByteArray, + packageName: String, + clientDataHash: ByteArray?, + convertedPrivateKey: ECPrivateKey, + credId: ByteArray, + ): BiometricPrompt { + val biometricPrompt = BiometricPrompt( + this, + mainExecutor, + object : AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, getString(R.string.authentication_error, errString)) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.e(TAG, getString(R.string.authentication_failed)) + finish() + } + + override fun onAuthenticationSucceeded( + result: AuthenticationResult, + ) { + super.onAuthenticationSucceeded(result) + + updatePasskeyInCredentialsDataSource(passkey) + + var callingOrigin = origin + if (callingAppInfoOrigin != null) { + callingOrigin = callingAppInfoOrigin + } + + configureGetCredentialResponse( + request, + origin = callingOrigin, + uid, + packageName, + clientDataHash, + convertedPrivateKey, + credId, + ) + } + }, + ) + return biometricPrompt + } + + /** + * To validate the user, surface a Biometric prompt (or other assertion method). + * + * @param biometricPrompt The BiometricPrompt object to use for authentication. + */ + private fun authenticate( + biometricPrompt: BiometricPrompt, + ) { + val promptInfo = Builder() + .setTitle(getString(R.string.use_your_screen_lock)) + .setSubtitle(getString(R.string.use_fingerprint)) + .setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL) + .build() + biometricPrompt.authenticate(promptInfo) + } + + /** + * Once the authentication succeeds, construct a JSON response based on the W3 Web Authentication Assertion spec. + * + * Construct a PublicKeyCredential using the JSON generated above and set it on a final GetCredentialResponse. + * + * Set this final response on the result of this activity. + * + * @param request The PublicKeyCredentialRequestOptions object. + * @param origin The origin of the calling app. + * @param uid The user ID. + * @param packageName The package name of the calling app. + * @param clientDataHash The client data hash. + * @param privateKey The private key. + * @param credId The credential ID. + */ + private fun configureGetCredentialResponse( + request: PublicKeyCredentialRequestOptions, + origin: String, + uid: ByteArray, + packageName: String, + clientDataHash: ByteArray?, + privateKey: ECPrivateKey, + credId: ByteArray, + ) { + val response = AuthenticatorAssertionResponse( + requestOptions = request, + origin = origin, + up = true, + uv = true, + be = true, + bs = true, + userHandle = uid, + packageName = packageName, + clientDataHash, + ) + + val signature = Signature.getInstance(getString(R.string.sha256_with_ecdsa)) + signature.initSign(privateKey) + signature.update(response.dataToSign()) + response.signature = signature.sign() + + val credential = FidoPublicKeyCredential( + rawId = credId, + response = response, + authenticatorAttachment = getString(R.string.platform), + ) + val intent = Intent() + val cred = PublicKeyCredential(credential.json()) + PendingIntentHandler.setGetCredentialResponse( + intent, + GetCredentialResponse(cred), + ) + setResult(RESULT_OK, intent) + finish() + } + + private fun updatePasskeyInCredentialsDataSource(passkeyItem: PasskeyItem) { + runBlocking { + credentialsDataSource.updatePasskey( + passkeyItem.copy( + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + } + + /** + * Encrypts the private key. This is used for demonstration purposes. + * + * @param privateKeyBytes The private key bytes to encrypt. + * @return The encrypted private key. + */ + private fun convertPrivateKey(privateKeyBytes: ByteArray): ECPrivateKey { + val params = AlgorithmParameters.getInstance(getString(R.string.ec)) + params.init(ECGenParameterSpec(getString(R.string.secp_256_r1))) + val spec = params.getParameterSpec(ECParameterSpec::class.java) + + // Convert the private key bytes to a BigInteger. + val bi = BigInteger(1, privateKeyBytes) + // Create an EC private key specification from the BigInteger and the EC parameter specification. + val privateKeySpec = ECPrivateKeySpec(bi, spec) + + val keyFactory = KeyFactory.getInstance(getString(R.string.ec)) + + // Generate the encrypted private key using the KeyFactory. + return keyFactory.generatePrivate(privateKeySpec) as ECPrivateKey + } + + companion object { + // This is to check if the origin was populated. + private const val INVALID_ALLOWLIST = "{\"apps\": [\n" + + " {\n" + + " \"type\": \"android\", \n" + + " \"info\": {\n" + + " \"package_name\": \"androidx.credentials.test\",\n" + + " \"signatures\" : [\n" + + " {\"build\": \"release\",\n" + + " \"cert_fingerprint_sha256\": \"HELLO\"\n" + + " },\n" + + " {\"build\": \"ud\",\n" + + " \"cert_fingerprint_sha256\": \"YELLOW\"\n" + + " }]\n" + + " }\n" + + " }\n" + + "]}\n" + + "\n" + private const val GPM_ALLOWLIST_URL = + "https://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json" + + private const val TAG = "MyVault" + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasswordActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasswordActivity.kt new file mode 100644 index 0000000..621bf82 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/GetPasswordActivity.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.enableEdgeToEdge +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPasswordOption +import androidx.credentials.PasswordCredential +import androidx.credentials.exceptions.NoCredentialException +import androidx.credentials.provider.PendingIntentHandler +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.PasswordItem +import kotlinx.coroutines.runBlocking +import java.time.Instant + +/** + * This class is responsible for handling the password credential (Passkey) get request from a Relying Party i.e calling app + */ +class GetPasswordActivity : ComponentActivity() { + private val credentialsDataSource = AppDependencies.credentialsDataSource + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + handleGetPasswordIntent() + } + + /** + * This method handles the GetPassword intent received from the calling app. + * + * @param getRequest The GetCredentialRequest object containing the request details. + */ + private fun handleGetPasswordIntent() { + val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) + + if (getRequest != null) { + val option = getRequest.credentialOptions[0] + + // Use the GetPasswordOption to retrieve password credentials for the incoming package name. + if (option is GetPasswordOption) { + val username = intent.getStringExtra(getString(R.string.key_account_id)) + try { + val credentials = + credentialsDataSource.credentialsForSite(getRequest.callingAppInfo.packageName) + + val passwords = credentials?.passwords + var passwordItem: PasswordItem? = null + val it = passwords?.iterator() + + var password = getString(R.string.empty) + + // Check in database if request password credential exist and return associated metadata + while (it?.hasNext() == true) { + val passwordItemCurrent = it.next() + if (passwordItemCurrent.username == username) { + passwordItem = passwordItemCurrent + password = passwordItemCurrent.password + break + } + } + + configureCredentialResponse(passwordItem, username, password) + } catch (e: Exception) { + // Catch exception + } + } + } + } + + /** + * This method configures the password credential response to be sent back to the calling app. + * + * @param passwordItem The PasswordItem object containing the password credential details. + * @param username The username associated with the password credential. + * @param password The password associated with the password credential. + */ + private fun configureCredentialResponse( + passwordItem: PasswordItem?, + username: String?, + password: String, + ) { + if (passwordItem == null) { + val result = Intent() + PendingIntentHandler.setGetCredentialException( + result, + NoCredentialException(), + ) + setResult(RESULT_OK, result) + this.finish() + } else { + // Update timestamp + runBlocking { + credentialsDataSource.updatePassword( + passwordItem.copy( + lastUsedTimeMs = Instant.now().toEpochMilli(), + ), + ) + } + + setIntentForGetCredentialResponse(username, password) + } + } + + /** + * This method sets the response for the selected password credential and finishes the flow. + * + * @param username The username associated with the password credential. + * @param password The password associated with the password credential. + */ + private fun setIntentForGetCredentialResponse(username: String?, password: String) { + val result = Intent() + val response = PasswordCredential(username.toString(), password) + PendingIntentHandler.setGetCredentialResponse( + result, + GetCredentialResponse(response), + ) + setResult(RESULT_OK, result) + this.finish() + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MainActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MainActivity.kt new file mode 100644 index 0000000..59e8dc8 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MainActivity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.core.view.WindowCompat +import com.example.android.authentication.myvault.ui.theme.MyVaultTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + MyVaultTheme { + MyVaultAppNavigation() + } + } + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultAppNavigation.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultAppNavigation.kt new file mode 100644 index 0000000..df7b0cf --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultAppNavigation.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Stateful version : Provides the state values to be password to the UI for top-level theming and navigation structure for the MyVault application. + * This composable manages the system UI appearance, navigation drawer, and the core + * navigation graph using Jetpack Compose. + */ +@Composable +fun MyVaultAppNavigation() { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val navController = rememberNavController() + val navigationActions = remember(navController) { + MyVaultNavActions(navController) + } + val coroutineScope = rememberCoroutineScope() + + MyVaultAppNavigation(drawerState, navController, navigationActions, coroutineScope) +} + +/** + * Stateless version : Provides the top-level theming and navigation structure for the MyVault application. + * This composable manages the system UI appearance, navigation drawer, and the core + * navigation graph using Jetpack Compose. + * + * @param drawerState Controls the open/closed state of the navigation drawer. + * @param navController The NavHostController used to manage navigation within the app. + * @param navigationActions Provides actions for common navigation events (home, settings, etc.). + * @param coroutineScope A CoroutineScope used to launch coroutines, primarily for drawer interactions. + */ +@Composable +fun MyVaultAppNavigation( + drawerState: DrawerState, + navController: NavHostController, + navigationActions: MyVaultNavActions, + coroutineScope: CoroutineScope, +) { + val systemUiController = rememberSystemUiController() + val barColor = MaterialTheme.colorScheme.surface + SideEffect { + systemUiController.setSystemBarsColor(barColor) + } + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = + navBackStackEntry?.destination?.route ?: MyVaultDestinations.HOME_ROUTE + + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(.7f), + ) { + AppDrawer( + currentRoute = currentRoute, + navigateToHome = navigationActions.navigateToHome, + navigateToSettings = navigationActions.navigateToSettings, + closeDrawer = { coroutineScope.launch { drawerState.close() } }, + ) + } + }, + drawerState = drawerState, + gesturesEnabled = drawerState.isOpen, + ) { + MyVaultNavGraph( + Modifier + .statusBarsPadding() + .navigationBarsPadding(), + navController = navController, + openDrawer = { + coroutineScope.launch { + drawerState.open() + } + }, + ) + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavActions.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavActions.kt new file mode 100644 index 0000000..3e212a5 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavActions.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController + +object MyVaultDestinations { + const val HOME_ROUTE = "home" + const val SETTINGS_ROUTE = "settings" +} + +/** + * Class that handles navigation actions for the MyVault app. + * + * @param navController The NavHostController used to navigate between destinations. + */ +class MyVaultNavActions(navController: NavHostController) { + /** + * Navigates to the home destination. + */ + val navigateToHome: () -> Unit = { + navController.navigate(MyVaultDestinations.HOME_ROUTE) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + + /** + * Navigates to the settings destination. + */ + val navigateToSettings: () -> Unit = { + navController.navigate(MyVaultDestinations.SETTINGS_ROUTE) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavGraph.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavGraph.kt new file mode 100644 index 0000000..8147036 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MyVaultNavGraph.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.ui.home.HomeScreen +import com.example.android.authentication.myvault.ui.home.HomeViewModelFactory +import com.example.android.authentication.myvault.ui.settings.SettingsScreen +import com.example.android.authentication.myvault.ui.settings.SettingsViewModelFactory + +/** + * Composable that represents the navigation graph for the MyVault app. + * + * @param modifier The modifier to be applied to the composable. + * @param navController The NavHostController used to navigate between destinations. + * @param openDrawer A function that opens the drawer. + * @param startDestination The route of the start destination. + */ +@Composable +fun MyVaultNavGraph( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + openDrawer: () -> Unit = {}, + startDestination: String = MyVaultDestinations.HOME_ROUTE, +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier.fillMaxHeight(), + ) { + composable(MyVaultDestinations.HOME_ROUTE) { + HomeScreen( + homeViewModel = viewModel( + factory = HomeViewModelFactory( + AppDependencies.credentialsDataSource, + AppDependencies.RPIconDataSource, + ), + ), + openDrawer = openDrawer, + ) + } + composable(MyVaultDestinations.SETTINGS_ROUTE) { + SettingsScreen( + viewModel = viewModel(factory = SettingsViewModelFactory(AppDependencies.database)), + openDrawer = openDrawer, + ) + } + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/UnlockActivity.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/UnlockActivity.kt new file mode 100644 index 0000000..56af005 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/UnlockActivity.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.enableEdgeToEdge +import androidx.biometric.BiometricManager.Authenticators +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo.Builder +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse +import androidx.credentials.provider.PendingIntentHandler +import androidx.fragment.app.FragmentActivity +import com.example.android.authentication.myvault.AppDependencies +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.CredentialsRepository + +/** + * Activity responsible for coordinating the secure unlock process of the MyVault application. + * This includes: + * * Handling biometric or device credential authentication. + * * Processing credential retrieval requests (using the CredentialsRepository). + * * Providing an appropriate response to the system after successful authentication. + */ +class UnlockActivity : FragmentActivity() { + + companion object { + private const val TAG = "MyVault" + } + + private lateinit var credentialsRepo: CredentialsRepository + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + credentialsRepo = CredentialsRepository( + AppDependencies.sharedPreferences, + AppDependencies.credentialsDataSource, + applicationContext, + ) + + val request = PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) + if (request != null) { + unlock(request) + } + } + + /** + * Initiates the biometric unlock process. + * + * @param request The BeginGetCredentialRequest obtained from the intent. + */ + private fun unlock(request: BeginGetCredentialRequest) { + val biometricPrompt = BiometricPrompt( + this, + mainExecutor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(errorCode, errString) + Log.e(TAG, getString(R.string.authentication_error, errString)) + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.e(TAG, getString(R.string.authentication_failed)) + finish() + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult, + ) { + super.onAuthenticationSucceeded(result) + // applocked to false + processGetCredentialRequest(request) + } + }, + ) + authenticate(biometricPrompt) + } + + /** + * Processes the BeginGetCredentialRequest, generating a response and finishing the activity. + * + * @param request The BeginGetCredentialRequest to process. + */ + private fun processGetCredentialRequest(request: BeginGetCredentialRequest) { + val authenticationResultIntent = Intent() + + val responseBuilder = BeginGetCredentialResponse.Builder() + + if (credentialsRepo.processGetCredentialsRequest(request, responseBuilder)) { + PendingIntentHandler.setBeginGetCredentialResponse( + authenticationResultIntent, + responseBuilder.build(), + ) + } + setResult(RESULT_OK, authenticationResultIntent) + finish() + } + + /** + * Configures and displays the biometric authentication prompt. + * + * @param biometricPrompt The BiometricPrompt instance used for authentication. + */ + private fun authenticate(biometricPrompt: BiometricPrompt) { + val promptInfo = Builder() + .setTitle(getString(R.string.unlock_app)) + .setSubtitle(getString(R.string.unlock_app_to_access_credentials)) + .setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL) + .build() + biometricPrompt.authenticate(promptInfo) + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/CredentialsList.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/CredentialsList.kt new file mode 100644 index 0000000..cefae41 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/CredentialsList.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.home + +import android.graphics.Bitmap +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.android.authentication.myvault.Dimensions +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.room.SiteWithCredentials + +/** + * This stateful composable holds the state values to pass into the CredentialsList Composable + * + * @param sites The list of sites with credentials. + * @param iconMap The map of site URLs to their corresponding icons. + * @param onSiteSelected The callback to be invoked when a site is selected. + * @param modifier The modifier to be applied to the composable. + */ +@Composable +fun CredentialsList( + sites: List, + iconMap: Map, + onSiteSelected: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(Dimensions.padding_medium), + modifier = modifier, + ) { + Text( + text = stringResource(R.string.your_saved_credentials_appear_here), + modifier = Modifier.padding(Dimensions.padding_large), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + HorizontalDivider(color = MaterialTheme.colorScheme.primary.copy(alpha = .2f)) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(Dimensions.padding_medium), + shape = RoundedCornerShape(5), + ) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy((-1).dp), + modifier = Modifier.background(MaterialTheme.colorScheme.background), + ) { + items(sites) { + CredentialEntry( + site = it, + iconMap[it.site.url], + onSiteSelected = onSiteSelected, + ) + } + } + } + } +} + +/** + * This stateless composable is for all the credentials saved in MyVault through different client apps. + * + * @param sites The list of sites with credentials. + * @param iconMap The map of site URLs to their corresponding icons. + * @param onSiteSelected The callback to be invoked when a site is selected + * @param modifier The modifier to be applied to the composable. + */ +@Composable +fun CredentialEntry( + site: SiteWithCredentials, + icon: Bitmap?, + onSiteSelected: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(Dimensions.padding_medium), + border = BorderStroke(.5.dp, MaterialTheme.colorScheme.outlineVariant), + elevation = CardDefaults.cardElevation( + defaultElevation = Dimensions.padding_small, + ), + onClick = { onSiteSelected(site.site.id) }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth(), + ) { + if (icon == null) { + Image( + modifier = Modifier + .padding(Dimensions.padding_medium) + .size(Dimensions.padding_extra_large, Dimensions.padding_extra_large), + imageVector = Icons.Filled.Lock, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), + contentDescription = stringResource(R.string.lock), + ) + } else { + Image( + modifier = Modifier + .padding(Dimensions.padding_medium) + .size(Dimensions.padding_extra_large, Dimensions.padding_extra_large), + bitmap = icon.asImageBitmap(), + contentDescription = site.site.name, + ) + } + Text( + text = site.site.url, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + } +} + +/** + * This composable function provides a preview of the CredentialsList composable. + */ +@Preview +@Composable +fun CredentialsListPreview() { + val list: List = emptyList() + val iconMap = emptyMap() + + CredentialsList( + sites = list, + iconMap = iconMap, + onSiteSelected = {}, + modifier = Modifier, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeScreen.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeScreen.kt new file mode 100644 index 0000000..5b953e3 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeScreen.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.home + +import android.graphics.Bitmap +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableLongState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.data.PasswordItem +import com.example.android.authentication.myvault.data.room.SiteWithCredentials + +/** + * This composable holds the stateful version of Home screen + * @param homeViewModel : viewmodel instance handling business logic for Home Screen + * @param openDrawer : method to open the drawer on click + * @param modifier The modifier to be applied to the composable. + */ +@Composable +fun HomeScreen( + homeViewModel: HomeViewModel, + openDrawer: () -> Unit, + modifier: Modifier = Modifier, +) { + val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() + val hasShownCredentials = rememberSaveable { mutableStateOf(false) } + val currentSiteId = rememberSaveable { mutableLongStateOf(0L) } + + HomeScreen( + openDrawer, + uiState, + homeViewModel::onPasskeyDelete, + homeViewModel::onPasswordDelete, + hasShownCredentials, + currentSiteId, + modifier, + ) +} + +/** + * This class holds the stateless version of Home screen to ease preview + * @param openDrawer : method to open the drawer on click + * @param uiState : MutableStateFlow to retrieve updated state from viewmodel + * @param onPasskeyDelete : Method to be called on passkey delete button click + * @param onPasswordDelete : Method to be called on password delete button click + * @param modifier The modifier to be applied to the composable. + */ +@Composable +fun HomeScreen( + openDrawer: () -> Unit, + uiState: HomeUiState, + onPasskeyDelete: (PasskeyItem) -> Unit, + onPasswordDelete: (PasswordItem) -> Unit, + hasShownCredentials: MutableState, + currentSiteId: MutableLongState, + modifier: Modifier = Modifier, +) { + val site = uiState.siteList.find { it.site.id == currentSiteId.longValue } + if (site != null && hasShownCredentials.value) { + ShowCredentialsScreen( + modifier = modifier, + site = site, + onCancel = { hasShownCredentials.value = false }, + onPasswordDelete = { + onPasswordDelete(it) + }, + onPasskeyDelete = { + onPasskeyDelete(it) + }, + ) + } else { + HomeScreenContent( + openDrawer = openDrawer, + sites = uiState.siteList, + iconMap = uiState.iconMap, + { siteId -> + currentSiteId.longValue = siteId + hasShownCredentials.value = true + }, + modifier, + ) + } +} + +/** + * This composable contain the UI logic rendered on Home screen + * + * @param openDrawer The method to open the drawer on click. + * @param sites The list of sites with credentials. + * @param iconMap The map of site names to their corresponding icons. + * @param onSiteSelected The callback to be invoked when a site is selected + * @param modifier The modifier to be applied to the composable. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreenContent( + openDrawer: () -> Unit, + sites: List, + iconMap: Map, + onSiteSelected: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + topBar = { + TopAppBarContent(openDrawer) + }, + modifier = modifier, + ) { innerPadding -> + CredentialsList( + sites = sites, + iconMap = iconMap, + onSiteSelected = onSiteSelected, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun TopAppBarContent(openDrawer: () -> Unit) { + CenterAlignedTopAppBar( + modifier = Modifier, + title = { + Text( + text = stringResource(R.string.credentials) + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigationIcon = { + IconButton(onClick = openDrawer) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = stringResource(R.string.credentials), + ) + } + }, + ) +} + +/** + * This composable function provides a preview of the HomeScreen composable. + */ +@Preview +@Composable +fun HomeScreenPreview() { + val hasShownCredentials = rememberSaveable { mutableStateOf(false) } + val currentSiteId = rememberSaveable { mutableLongStateOf(0L) } + + HomeScreen( + openDrawer = {}, + uiState = HomeUiState(), + onPasswordDelete = {}, + onPasskeyDelete = {}, + hasShownCredentials = hasShownCredentials, + currentSiteId = currentSiteId, + modifier = Modifier, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModel.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..3e36919 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.home + +import android.graphics.Bitmap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.android.authentication.myvault.data.CredentialsDataSource +import com.example.android.authentication.myvault.data.RPIconDataSource +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.data.PasswordItem +import com.example.android.authentication.myvault.data.room.SiteWithCredentials +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * This class is a ViewModel that holds the business logic to operate on a list of credentials. + * @param credentialsDataSource The data source for credentials. + * @param RPIconDataSource The data source for rpicons. + */ +class HomeViewModel( + private val credentialsDataSource: CredentialsDataSource, + private val RPIconDataSource: RPIconDataSource, +) : ViewModel() { + + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + /** + * Removes the associated password from the database. + * + * @param password The password to remove. + */ + fun onPasswordDelete(password: PasswordItem) { + viewModelScope.launch { + credentialsDataSource.removePassword(password) + } + } + + /** + * Removes the associated passkey credential from the database. + * + * @param passkey The passkey credential to remove. + */ + fun onPasskeyDelete(passkey: PasskeyItem) { + viewModelScope.launch { + credentialsDataSource.removePasskey(passkey) + } + } + + // Initialize the home screen with list of saved credentials from calling apps. + init { + viewModelScope.launch { + credentialsDataSource.siteListWithCredentials().collect { siteList -> + // Get the icons + val icons: MutableMap = mutableMapOf() + siteList.forEach { + val icon = RPIconDataSource.getIcon(it.site.url) + if (icon != null) { + icons[it.site.url] = icon + } + } + _uiState.value = HomeUiState(siteList = siteList, iconMap = icons) + } + } + } +} + +data class HomeUiState( + val siteList: List = emptyList(), + val iconMap: Map = emptyMap(), +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModelFactory.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModelFactory.kt new file mode 100644 index 0000000..81904ac --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/HomeViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.android.authentication.myvault.data.CredentialsDataSource +import com.example.android.authentication.myvault.data.RPIconDataSource + +/** + * This class is a factory for creating instances of the {@link HomeViewModel} class. + * + *

This factory is used by the {@link ViewModelProvider} to create instances of the {@link + * HomeViewModel} class. The factory takes two parameters, {@code credentialsDataSource} and {@code + * rpIconDataSource}, which are used to initialize the {@link HomeViewModel} instance. + */ +class HomeViewModelFactory( + private val credentialsDataSource: CredentialsDataSource, + private val RPIconDataSource: RPIconDataSource, +) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return HomeViewModel(credentialsDataSource, RPIconDataSource) as T + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/ShowCredentialsScreen.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/ShowCredentialsScreen.kt new file mode 100644 index 0000000..25081e0 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/home/ShowCredentialsScreen.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.home + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.android.authentication.myvault.Dimensions +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.data.PasskeyItem +import com.example.android.authentication.myvault.data.PasswordItem +import com.example.android.authentication.myvault.data.room.SiteMetaData +import com.example.android.authentication.myvault.data.room.SiteWithCredentials + +/** + * This composable holds the UI logic to show credential details of selected domain/calling app + * @param site : selected domain/site + * @param onCancel : method to call on back press + * @param onPasswordDelete : method to call on selected password credential delete + * @param onPasskeyDelete : method to call on selected passkey credential delete + * @param modifier : modifier for the composable + */ +@Composable +fun ShowCredentialsScreen( + site: SiteWithCredentials, + onCancel: () -> Unit, + onPasswordDelete: (PasswordItem) -> Unit, + onPasskeyDelete: (PasskeyItem) -> Unit, + modifier: Modifier = Modifier, +) { + val snackbarHostState = remember { SnackbarHostState() } + + ShowCredentialsScreen( + snackbarHostState, + site, + onCancel, + onPasswordDelete, + onPasskeyDelete, + modifier, + ) +} + +/** + * This composable holds the UI logic to show credential details of selected domain/calling app + * + * @param snackbarHostState The state of the SnackbarHost + * @param site The selected domain/site + * @param onCancel The callback to be invoked when the user clicks the back button + * @param onPasswordDelete The callback to be invoked when the user clicks the delete button for a password credential + * @param onPasskeyDelete The callback to be invoked when the user clicks the delete button for a passkey credential + * @param modifier The modifier to be applied to the composable + */ +@Composable +fun ShowCredentialsScreen( + snackbarHostState: SnackbarHostState, + site: SiteWithCredentials, + onCancel: () -> Unit, + onPasswordDelete: (PasswordItem) -> Unit, + onPasskeyDelete: (PasskeyItem) -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler { + onCancel() + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBarContent(site, onCancel, Modifier) + }, + modifier = modifier, + ) { innerPadding -> + CredentialsEntry(innerPadding, site, onPasskeyDelete, onPasswordDelete, Modifier) + } +} + +/** + * This composable holds the UI logic to show the top app bar with the site name and a back button. + * + * @param site The selected domain/site + * @param onCancel The callback to be invoked when the user clicks the back button + * @param modifier The modifier to be applied to the composable + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun TopAppBarContent( + site: SiteWithCredentials, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.credentials_for, site.site.url), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigationIcon = { + IconButton(onClick = onCancel) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + }, + ) +} + +/** + * Renders the credential entries for the selected domain/site. + * + * @param innerPadding The padding to apply to the inner content. + * @param site The SiteWithCredentials object representing the site and its credentials. + * @param onPasskeyDelete The callback to be invoked when a passkey is deleted. + * @param onPasswordDelete The callback to be invoked when a password is deleted. + * @param modifier The modifier to be applied to the composable. + */ +@Composable +private fun CredentialsEntry( + innerPadding: PaddingValues, + site: SiteWithCredentials, + onPasskeyDelete: (PasskeyItem) -> Unit, + onPasswordDelete: (PasswordItem) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .padding(innerPadding) + .fillMaxWidth() + .padding(Dimensions.padding_large) + .background(MaterialTheme.colorScheme.background), + horizontalAlignment = Alignment.CenterHorizontally, + + ) { + items(site.passkeys) { + PasskeyEntry( + passkey = it, + onPasskeyDelete = onPasskeyDelete, + Modifier, + ) + } + items(site.passwords) { + PasswordEntry( + password = it, + onPasswordDelete = onPasswordDelete, + Modifier, + ) + } + } +} + +/** + * Renders the password entry for the selected domain/site. + * + * @param modifier The modifier to be applied to the composable. + * @param password The password item to display. + * @param onPasswordDelete The callback to be invoked when the user clicks the delete button. + * @param modifier The modifier to be applied to the composable + */ +@Composable +fun PasswordEntry( + password: PasswordItem, + onPasswordDelete: (PasswordItem) -> Unit, + modifier: Modifier = Modifier, +) { + var passwordVisible by rememberSaveable { mutableStateOf(false) } + Card( + modifier = modifier + .fillMaxWidth() + .padding(top = Dimensions.padding_medium), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + shape = MaterialTheme.shapes.large, + ) { + Column( + modifier = Modifier.padding(Dimensions.padding_medium), + verticalArrangement = Arrangement.spacedBy(Dimensions.padding_medium), + ) { + TextField( + value = password.username, + onValueChange = {}, + readOnly = true, + shape = MaterialTheme.shapes.extraLarge, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + unfocusedTextColor = MaterialTheme.colorScheme.outline, + ), + ) + TextField( + value = password.password, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + onValueChange = {}, + readOnly = true, + shape = MaterialTheme.shapes.extraLarge, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + unfocusedTextColor = MaterialTheme.colorScheme.outline, + ), + trailingIcon = { + val text = if (passwordVisible) { + stringResource(R.string.hide) + } else { + stringResource(R.string.show) + } + ClickableText( + style = TextStyle( + color = MaterialTheme.colorScheme.primary, + ), + text = AnnotatedString(text), + onClick = { passwordVisible = !passwordVisible }, + ) + }, + ) + Button( + modifier = Modifier + .padding(horizontal = Dimensions.padding_small) + .align(Alignment.End), + onClick = { onPasswordDelete(password) }, + ) { + Text(text = stringResource(R.string.delete)) + } + } + } +} + +/** + * This composable holds the UI logic to render a single passkey entry. + * + * @param passkey The PasskeyItem object representing the passkey + * @param onPasskeyDelete The callback to be invoked when the user clicks the delete button + * @param modifier The modifier to be applied to the composable + */ +@Composable +fun PasskeyEntry( + passkey: PasskeyItem, + onPasskeyDelete: (PasskeyItem) -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(top = Dimensions.padding_medium), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + shape = MaterialTheme.shapes.large, + ) { + Column( + modifier = Modifier.padding(Dimensions.padding_medium), + verticalArrangement = Arrangement.spacedBy(Dimensions.padding_medium), + ) { + TextField( + value = passkey.username, + onValueChange = {}, + readOnly = true, + shape = MaterialTheme.shapes.extraLarge, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + unfocusedTextColor = MaterialTheme.colorScheme.outline, + ), + ) + Button( + modifier = Modifier + .padding(horizontal = Dimensions.padding_small) + .align(Alignment.End), + onClick = { onPasskeyDelete(passkey) }, + ) { + Text(text = stringResource(R.string.delete)) + } + } + } +} + +/** + * This composable function provides a preview of the ShowCredentialsScreen composable. + */ +@Preview +@Composable +fun ShowCredentialsScreenPreview() { + ShowCredentialsScreen( + snackbarHostState = SnackbarHostState(), + onCancel = {}, + onPasswordDelete = {}, + onPasskeyDelete = {}, + site = SiteWithCredentials(SiteMetaData(), emptyList(), emptyList()), + modifier = Modifier, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/password/PasswordScreen.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/password/PasswordScreen.kt new file mode 100644 index 0000000..955c4e6 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/password/PasswordScreen.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.password + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.android.authentication.myvault.Dimensions +import com.example.android.authentication.myvault.R +import com.example.android.authentication.myvault.ui.theme.MyVaultTheme + +/** + * This stateful composable holds the state values to be passed to the UI displayed to user while saving/updating the password credential for selected site + * + * @param onSave The callback to be invoked when the user clicks the "Save" button + * @param modifier The modifier to be applied to the composable + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PasswordScreen( + onSave: () -> Unit, + modifier: Modifier = Modifier, +) { + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + PasswordScreen( + onSave, + bottomSheetState, + modifier, + ) +} + +/** + * This stateless composable is for the UI displayed to user while saving/updating the password credential for selected site + * + * @param onSave The callback to be invoked when the user clicks the "Save" button + * @param bottomSheetState The state of the bottom sheet + * @param modifier The modifier to be applied to the composable + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PasswordScreen( + onSave: () -> Unit, + bottomSheetState: SheetState, + modifier: Modifier = Modifier, +) { + var showBottomSheet by rememberSaveable { mutableStateOf(true) } + + MyVaultTheme { + if (showBottomSheet) { + ModalBottomSheet( + modifier = modifier, + sheetState = bottomSheetState, + containerColor = MaterialTheme.colorScheme.onPrimary, + content = { + Card( + modifier = Modifier + .heightIn(min = 20.dp) + .padding(Dimensions.padding_medium) + .background(MaterialTheme.colorScheme.onPrimary), + ) { + SaveYourPasswordCard(onSave, Modifier) + } + }, + shape = MaterialTheme.shapes.medium, + onDismissRequest = { + showBottomSheet = false + }, + ) + } + } +} + +@Composable +private fun SaveYourPasswordCard(onSave: () -> Unit, modifier: Modifier = Modifier) { + Column(modifier.background(MaterialTheme.colorScheme.onPrimary)) { + Icon( + painter = painterResource(R.drawable.android_secure), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .size(80.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(R.string.save_your_password_to_vault), + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = Dimensions.padding_medium), + ) + Button( + modifier = Modifier + .fillMaxWidth() + .padding(Dimensions.padding_extra_large) + .align(Alignment.End), + onClick = onSave, + shape = MaterialTheme.shapes.medium, + ) { + Text( + text = stringResource(R.string.text_continue), + style = MaterialTheme.typography.titleSmall, + ) + } + } +} + +/** + * This composable function provides a preview of the PasswordScreen composable. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun PasswordScreenPreview() { + PasswordScreen( + onSave = { }, + bottomSheetState = rememberModalBottomSheetState(false), + modifier = Modifier, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsScreen.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..4f16a48 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsScreen.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.settings + +import android.util.Log +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.android.authentication.myvault.R + +/** + * This composable holds the stateful version of Settings screen + * @param viewModel : viewmodel instance handling business logic for Settings Screen + * @param openDrawer : method to open the drawer on click + * @param modifier : Modifier to update behavior of composables UI + */ +@Composable +fun SettingsScreen( + openDrawer: () -> Unit, + viewModel: SettingsViewModel, + modifier: Modifier = Modifier, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + val snackbarHostState = remember { SnackbarHostState() } + + SettingsScreen( + viewModel::deleteAllData, + openDrawer, + uiState, + snackbarHostState, + modifier, + ) +} + +/** + * This composable holds the stateless version of Home screen to ease preview + * @param openDrawer : method to open the drawer on click + * @param onDeleteClicked : Method to be called on "Delete all credentials" click + * @param uiState : MutableStateFlow to retrieve updated state from viewmodel + * @param snackbarHostState : State of the SnackbarHost, which controls the queue and the current Snackbar being shown inside + * @param modifier : Modifier to update behavior of composables UI + */ +@Composable +fun SettingsScreen( + onDeleteClicked: () -> Unit, + openDrawer: () -> Unit, + uiState: SettingsViewModel.UiState, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + AppBarContent(openDrawer, Modifier) + }, + modifier = modifier, + ) { innerPadding -> + DeleteCredentialsButton(onDeleteClicked, innerPadding, Modifier) + } + when (uiState) { + is SettingsViewModel.UiState.Init -> { + Log.w(stringResource(R.string.myvault), stringResource(R.string.initialized)) + } + + is SettingsViewModel.UiState.Success -> { + LaunchedEffect(uiState) { + snackbarHostState.showSnackbar( + context.getString(R.string.data_deleted), + null, + false, + SnackbarDuration.Short, + ) + } + } + } +} + +/** + * Set the top AppBar UI + * + * @param openDrawer : method to open the drawer on click + * @param modifier Modifier to update behavior of composables UI + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun AppBarContent(openDrawer: () -> Unit, modifier: Modifier = Modifier) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = stringResource(R.string.settings) + ) + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + navigationIcon = { + IconButton(onClick = openDrawer) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = stringResource(R.string.credentials), + ) + } + }, + ) +} + +/** + * Set the Delete Button UI & action + * + * @param onDeleteClicked Method to be called on "Delete all credentials" click + * @param innerPadding PaddingValues to apply to the button + * @param modifier Modifier to update behavior of composables UI + */ +@Composable +private fun DeleteCredentialsButton( + onDeleteClicked: () -> Unit, + innerPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier + .fillMaxSize() + .wrapContentSize(Alignment.TopCenter) + .padding(innerPadding), + onClick = { + onDeleteClicked() + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text(text = stringResource(R.string.delete_all_data)) + } +} + +/** + * This composable function provides a preview of the SettingsScreen composable. + */ +@Preview +@Composable +fun SettingsScreenPreview() { + SettingsScreen( + onDeleteClicked = { }, + openDrawer = {}, + uiState = SettingsViewModel.UiState.Init, + snackbarHostState = SnackbarHostState(), + modifier = Modifier, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModel.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..7d4c669 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.settings + +import androidx.lifecycle.ViewModel +import com.example.android.authentication.myvault.data.room.MyVaultDatabase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * This viewmodel holds the logic for deleting all the credentials saved on MyVault + */ +class SettingsViewModel(private val database: MyVaultDatabase) : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Init) + val uiState: StateFlow = _uiState.asStateFlow() + + /** + * Deletes all the data from the database. + */ + fun deleteAllData() { + database.clearAllTables() + _uiState.update { + UiState.Success + } + } + + /** + * Represents the different states of the Settings screen. + */ + sealed class UiState { + + /** + * The initial state of the screen. + */ + data object Init : UiState() + + /** + * The state after the data has been deleted successfully. + */ + data object Success : UiState() + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModelFactory.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModelFactory.kt new file mode 100644 index 0000000..79bbe89 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/settings/SettingsViewModelFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.android.authentication.myvault.data.room.MyVaultDatabase + +/** + * This class is a factory for creating instances of the {@link SettingsViewModel} class. + * + *

This factory is used by the {@link ViewModelProvider} to create instances of the {@link + * SettingsViewModel} class. The factory takes one parameter, {@code database}, which is used to + * initialize the {@link SettingsViewModel} instance. + */ +class SettingsViewModelFactory( + private val database: MyVaultDatabase, +) : ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T { + return SettingsViewModel(database) as T + } +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Color.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Color.kt new file mode 100644 index 0000000..47f095e --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Color.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.theme + +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFF6750A4) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFE9DDFF) +val md_theme_light_onPrimaryContainer = Color(0xFF22005D) +val md_theme_light_secondary = Color(0xFF625B71) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) +val md_theme_light_onSecondaryContainer = Color(0xFF1E192B) +val md_theme_light_tertiary = Color(0xFF7E5260) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFD9E3) +val md_theme_light_onTertiaryContainer = Color(0xFF31101D) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFFFBFF) +val md_theme_light_onBackground = Color(0xFF1C1B1E) +val md_theme_light_surface = Color(0xFFFFFBFF) +val md_theme_light_onSurface = Color(0xFF1C1B1E) +val md_theme_light_surfaceVariant = Color(0xFFE7E0EB) +val md_theme_light_onSurfaceVariant = Color(0xFF49454E) +val md_theme_light_outline = Color(0xFF7A757F) +val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4) +val md_theme_light_inverseSurface = Color(0xFF313033) +val md_theme_light_inversePrimary = Color(0xFFCFBCFF) +val md_theme_light_surfaceTint = Color(0xFF6750A4) +val md_theme_light_outlineVariant = Color(0xFFCAC4CF) +val md_theme_light_scrim = Color(0x00000000) + +val md_theme_dark_primary = Color(0xFFCFBCFF) +val md_theme_dark_onPrimary = Color(0xFF381E72) +val md_theme_dark_primaryContainer = Color(0xFF4F378A) +val md_theme_dark_onPrimaryContainer = Color(0xFFE9DDFF) +val md_theme_dark_secondary = Color(0xFFCBC2DB) +val md_theme_dark_onSecondary = Color(0xFF332D41) +val md_theme_dark_secondaryContainer = Color(0xFF4A4458) +val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) +val md_theme_dark_tertiary = Color(0xFFEFB8C8) +val md_theme_dark_onTertiary = Color(0xFF4A2532) +val md_theme_dark_tertiaryContainer = Color(0xFF633B48) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E3) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF1C1B1E) +val md_theme_dark_onBackground = Color(0xFFE6E1E6) +val md_theme_dark_surface = Color(0xFF1C1B1E) +val md_theme_dark_onSurface = Color(0xFFE6E1E6) +val md_theme_dark_surfaceVariant = Color(0xFF49454E) +val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4CF) +val md_theme_dark_outline = Color(0xFF948F99) +val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1E) +val md_theme_dark_inverseSurface = Color(0xFFE6E1E6) +val md_theme_dark_inversePrimary = Color(0xFF6750A4) +val md_theme_dark_surfaceTint = Color(0xFFCFBCFF) +val md_theme_dark_outlineVariant = Color(0xFF49454E) +val md_theme_dark_scrim = Color(0x00000000) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Shape.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Shape.kt new file mode 100644 index 0000000..d3be780 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Shape.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(8.dp), + large = RoundedCornerShape(5.dp), + extraLarge = RoundedCornerShape(10.dp), +) diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Theme.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Theme.kt new file mode 100644 index 0000000..532967a --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Theme.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun MyVaultTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: + @Composable() + () -> Unit, +) { + val colorScheme = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colorScheme, + content = content, + shapes = Shapes, + typography = typography, + ) +} diff --git a/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Typography.kt b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Typography.kt new file mode 100644 index 0000000..a31d552 --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/theme/Typography.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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 + * + * https://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.authentication.myvault.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp + +val typography = Typography( + titleLarge = TextStyle(fontSize = 20.sp), + titleMedium = TextStyle(fontSize = 16.sp), +) diff --git a/CredentialProvider/MyVault/app/src/main/res/drawable/android_secure.xml b/CredentialProvider/MyVault/app/src/main/res/drawable/android_secure.xml new file mode 100644 index 0000000..b1dbb1e --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/res/drawable/android_secure.xml @@ -0,0 +1,27 @@ + + + + diff --git a/CredentialProvider/MyVault/app/src/main/res/values/strings.xml b/CredentialProvider/MyVault/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..672a1dc --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/res/values/strings.xml @@ -0,0 +1,64 @@ + + + MyVault + My Vault + username + key_account_id + Use your screen lock + Use your fingerprint to continue + SHA256withECDSA + platform + EC + secp256r1 + VAULT_DATA + IS_AUTO_SELECTED + All Data Deleted + credId + Authentication error: %1$s + Authentication failed + + Unlock app to access credentials + Unlock app + Settings + Credentials + Delete All Data + You are saving your password to MyVault. + Website + emailIcon + password + passwordIcon + Done + Back + Edit + Delete + Personal Info + Search + Passwords: %1$s Passkeys: %2$s + web + Provider Sample\n + Unable to retrieve data from intent + Manage Credentials + Open %1$s + lock + List of app credentials saved in MyVault from calling apps will appear here. Check the project\'s README for more details on how to enable MyVault in your device settings & save/retrieve credentials through it. + Credentials for %1$s + Continue + MyVault + Initialized + HIDE + SHOW + diff --git a/CredentialProvider/MyVault/app/src/main/res/values/themes.xml b/CredentialProvider/MyVault/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bc5c8dc --- /dev/null +++ b/CredentialProvider/MyVault/app/src/main/res/values/themes.xml @@ -0,0 +1,19 @@ + + + +