Re-implement lock screen feature using our own implementation.
This commit is contained in:
parent
32c6281dd2
commit
b5aedd4626
|
@ -16,4 +16,4 @@
|
||||||
/fastlane/private
|
/fastlane/private
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
|
|
||||||
/library/build
|
/**/build
|
||||||
|
|
|
@ -252,11 +252,7 @@ dependencyAnalysis {
|
||||||
exclude("org.json:json") // Used in unit tests, overwrites the one bundled into Android
|
exclude("org.json:json") // Used in unit tests, overwrites the one bundled into Android
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project(":library:ui-styles") {
|
project(":library:ui-styles")
|
||||||
onUnusedDependencies {
|
|
||||||
exclude("com.github.vector-im:PFLockScreen-Android") // False positive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
project(":matrix-sdk-android") {
|
project(":matrix-sdk-android") {
|
||||||
onUnusedDependencies {
|
onUnusedDependencies {
|
||||||
exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx
|
exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Improve lock screen implementation.
|
|
@ -24,11 +24,13 @@ def excludes = [
|
||||||
|
|
||||||
def initializeReport(report, projects, classExcludes) {
|
def initializeReport(report, projects, classExcludes) {
|
||||||
projects.each { project -> project.apply plugin: 'jacoco' }
|
projects.each { project -> project.apply plugin: 'jacoco' }
|
||||||
report.executionData { fileTree(rootProject.rootDir.absolutePath).include(
|
|
||||||
"**/build/outputs/unit_test_code_coverage/**/*.exec",
|
|
||||||
"**/build/outputs/code_coverage/**/coverage.ec"
|
|
||||||
) }
|
|
||||||
|
|
||||||
|
report.executionData {
|
||||||
|
fileTree(rootProject.rootDir.absolutePath).include(
|
||||||
|
"**/build/outputs/unit_test_code_coverage/**/*.exec",
|
||||||
|
"**/build/outputs/code_coverage/**/coverage.ec",
|
||||||
|
)
|
||||||
|
}
|
||||||
report.reports {
|
report.reports {
|
||||||
xml.enabled true
|
xml.enabled true
|
||||||
html.enabled true
|
html.enabled true
|
||||||
|
|
|
@ -28,13 +28,13 @@ def bigImageViewer = "1.8.1"
|
||||||
def jjwt = "0.11.5"
|
def jjwt = "0.11.5"
|
||||||
def vanniktechEmoji = "0.15.0"
|
def vanniktechEmoji = "0.15.0"
|
||||||
|
|
||||||
|
def fragment = "1.4.1"
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
||||||
def espresso = "3.4.0"
|
def espresso = "3.4.0"
|
||||||
def androidxTest = "1.4.0"
|
def androidxTest = "1.4.0"
|
||||||
def androidxOrchestrator = "1.4.1"
|
def androidxOrchestrator = "1.4.1"
|
||||||
|
|
||||||
|
|
||||||
ext.libs = [
|
ext.libs = [
|
||||||
gradle : [
|
gradle : [
|
||||||
'gradlePlugin' : "com.android.tools.build:gradle:$gradle",
|
'gradlePlugin' : "com.android.tools.build:gradle:$gradle",
|
||||||
|
@ -50,11 +50,14 @@ ext.libs = [
|
||||||
androidx : [
|
androidx : [
|
||||||
'annotation' : "androidx.annotation:annotation:1.3.0",
|
'annotation' : "androidx.annotation:annotation:1.3.0",
|
||||||
'activity' : "androidx.activity:activity:1.4.0",
|
'activity' : "androidx.activity:activity:1.4.0",
|
||||||
|
'annotations' : "androidx.annotation:annotation:1.3.0",
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
||||||
|
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||||
'core' : "androidx.core:core-ktx:1.8.0",
|
'core' : "androidx.core:core-ktx:1.8.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
||||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
|
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
|
||||||
|
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
|
||||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
|
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
|
||||||
'work' : "androidx.work:work-runtime-ktx:2.7.1",
|
'work' : "androidx.work:work-runtime-ktx:2.7.1",
|
||||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
||||||
|
@ -85,6 +88,7 @@ ext.libs = [
|
||||||
'dagger' : "com.google.dagger:dagger:$dagger",
|
'dagger' : "com.google.dagger:dagger:$dagger",
|
||||||
'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger",
|
'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger",
|
||||||
'hilt' : "com.google.dagger:hilt-android:$dagger",
|
'hilt' : "com.google.dagger:hilt-android:$dagger",
|
||||||
|
'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger",
|
||||||
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
|
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
|
@ -155,3 +159,5 @@ ext.libs = [
|
||||||
'junit' : "junit:junit:4.13.2"
|
'junit' : "junit:junit:4.13.2"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -56,8 +56,6 @@ dependencies {
|
||||||
implementation libs.google.material
|
implementation libs.google.material
|
||||||
// Pref theme
|
// Pref theme
|
||||||
implementation libs.androidx.preferenceKtx
|
implementation libs.androidx.preferenceKtx
|
||||||
// PFLockScreen attrs
|
|
||||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
|
||||||
// dialpad dimen
|
// dialpad dimen
|
||||||
implementation 'im.dlg:android-dialer:1.2.5'
|
implementation 'im.dlg:android-dialer:1.2.5'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||||
|
<gradient
|
||||||
|
android:type="linear"
|
||||||
|
android:startColor="#f28433"
|
||||||
|
android:endColor="#e0574c"
|
||||||
|
android:angle="270" />
|
||||||
|
|
||||||
|
</shape>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="#44FFFFFF"/>
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="70dp"
|
||||||
|
android:height="70dp"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:color="@color/lockscreen_code"
|
||||||
|
android:width="1px"/>
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="@dimen/lockscreen_code_size"
|
||||||
|
android:height="@dimen/lockscreen_code_size"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:color="@color/lockscreen_code"
|
||||||
|
android:width="1px"/>
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="@color/lockscreen_code"/>
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="@dimen/lockscreen_code_size"
|
||||||
|
android:height="@dimen/lockscreen_code_size"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<padding android:padding="1dp" />
|
||||||
|
<corners android:radius="5dp" />
|
||||||
|
<solid android:color="#44FFFFFF" />
|
||||||
|
</shape>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- NOTE: order is important (the first matching state(s) is what is rendered) -->
|
||||||
|
<item
|
||||||
|
android:state_checked="true"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_code_fill"/>
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/lockscreen_circle_code_empty"/>
|
||||||
|
</selector>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<vector android:height="14.498462dp" android:viewportHeight="589"
|
||||||
|
android:viewportWidth="975" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:pathData="M951,24H302.88L34,294L302.88,565H951V24Z"
|
||||||
|
android:strokeColor="#000000" android:strokeWidth="48"/>
|
||||||
|
<path android:pathData="M411.5,120L757.5,467.5M757.5,120L411.5,467.5"
|
||||||
|
android:strokeColor="#000000" android:strokeWidth="48"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#000000" android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/lockscreen_white_selector">
|
||||||
|
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<padding android:padding="1dp" />
|
||||||
|
<corners android:radius="5dp" />
|
||||||
|
<solid android:color="@color/lockscreen_white_selector"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<selector>
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<color android:color="@android:color/darker_gray"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_activated="true">
|
||||||
|
<color android:color="@android:color/white"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<color android:color="@android:color/transparent"/>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||||
|
|
||||||
|
<item android:state_focused="true" android:state_enabled="false" android:state_pressed="true"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_key_selector" />
|
||||||
|
<item android:state_focused="true" android:state_enabled="false"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_key_selector" />
|
||||||
|
<item android:state_focused="true" android:state_pressed="true"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_key_selector" />
|
||||||
|
<item android:state_focused="false" android:state_pressed="true"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_key_selector" />
|
||||||
|
<item android:state_focused="true"
|
||||||
|
android:drawable="@drawable/lockscreen_circle_key_selector" />
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/lockscreen_circle_background" />
|
||||||
|
</selector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="lockscreen_button_size">60dp</dimen>
|
||||||
|
<dimen name="lockscreen_button_margin_vertical">15dp</dimen>
|
||||||
|
</resources>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<attr name="lockscreen_key_button_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_fingerprint_button_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_delete_button_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_code_view_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_title_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_subtitle_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_hint_theme" format="reference|integer"/>
|
||||||
|
<attr name="lockscreen_next_theme" format="reference|integer"/>
|
||||||
|
</resources>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="lockscreen_code">#ffffff</color>
|
||||||
|
<color name="lockscreen_white_selector">#66ffffff</color>
|
||||||
|
<color name="lockscreen_hint_color">#42000000</color>
|
||||||
|
<color name="lockscreen_warning_color">#f4511e</color>
|
||||||
|
<color name="lockscreen_success_color">#009688</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="lockscreen_button_size">70dp</dimen>
|
||||||
|
<dimen name="lockscreen_button_margin_vertical">25dp</dimen>
|
||||||
|
<dimen name="lockscreen_code_size">10dp</dimen>
|
||||||
|
<dimen name="lockscreen_code_margin">5dp</dimen>
|
||||||
|
</resources>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<resources>
|
||||||
|
<string name="lockscreen_cancel">Cancel</string>
|
||||||
|
<string name="lockscreen_use_pin">Use pin</string>
|
||||||
|
<string name="lockscreen_sign_in">Sign in</string>
|
||||||
|
<string name="lockscreen_next">Next</string>
|
||||||
|
<string name="lockscreen_forgot">Forgot?</string>
|
||||||
|
<string name="lockscreen_title">Input pin code or use biometric authentication</string>
|
||||||
|
<string name="lockscreen_fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
|
||||||
|
<string name="lockscreen_fingerprint_success">Fingerprint recognized</string>
|
||||||
|
|
||||||
|
<string name="lockscreen_fingerprint_description">Confirm fingerprint to continue</string>
|
||||||
|
<string name="lockscreen_fingerprint_hint">Touch sensor</string>
|
||||||
|
<string name="lockscreen_description_fingerprint_icon">Fingerprint icon</string>
|
||||||
|
<string name="lockscreen_confirm_pin">Confirm PIN</string>
|
||||||
|
<string name="lockscreen_description_logo">Logo</string>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="LockScreenStyle">
|
||||||
|
<item name="android:background">@drawable/lockscreen_background</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenButtonStyle" parent="Theme.AppCompat.Light">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="android:textColor">@android:color/white</item>
|
||||||
|
<item name="android:background">@drawable/lockscreen_touch_selector</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenFingerPrintButtonStyle">
|
||||||
|
<item name="android:src">@drawable/lockscreen_fingerprint</item>
|
||||||
|
<item name="android:padding">20dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenDeleteButtonStyle">
|
||||||
|
<item name="android:src">@drawable/lockscreen_delete</item>
|
||||||
|
<item name="android:padding">20dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="CheckBox">
|
||||||
|
<item name="android:checkboxStyle">@style/LockScreenCodeStyle</item>
|
||||||
|
<item name="checkboxStyle">@style/LockScreenCodeStyle</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenCodeStyle">
|
||||||
|
<item name="android:button">@drawable/lockscreen_code_selector</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenNextTextStyle">
|
||||||
|
<item name="android:textColor">#9FFF</item>
|
||||||
|
<item name="android:textSize">18sp</item>
|
||||||
|
<item name="android:backgroundTint">#c66</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenHintTextStyle">
|
||||||
|
<item name="android:textColor">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="LockScreenTitleTextStyle">
|
||||||
|
<item name="android:textColor">@android:color/white</item>
|
||||||
|
<item name="android:textSize">18sp</item>
|
||||||
|
<item name="android:gravity">center</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -22,13 +22,13 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PinCodeDeleteButtonStyle">
|
<style name="PinCodeDeleteButtonStyle">
|
||||||
<item name="android:src">@drawable/delete_lockscreen_pf</item>
|
<item name="android:src">@drawable/lockscreen_delete</item>
|
||||||
<item name="android:tint">?vctr_content_primary</item>
|
<item name="android:tint">?vctr_content_primary</item>
|
||||||
<item name="background">@drawable/bg_pin_key</item>
|
<item name="background">@drawable/bg_pin_key</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PinCodeFingerprintButtonStyle">
|
<style name="PinCodeFingerprintButtonStyle">
|
||||||
<item name="android:src">@drawable/fingerprint_lockscreen_pf</item>
|
<item name="android:src">@drawable/lockscreen_fingerprint</item>
|
||||||
<item name="android:tint">?vctr_content_primary</item>
|
<item name="android:tint">?vctr_content_primary</item>
|
||||||
<item name="background">@drawable/bg_pin_key</item>
|
<item name="background">@drawable/bg_pin_key</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -111,14 +111,14 @@
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||||
|
|
||||||
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
<item name="lockscreen_theme">@style/PinCodeScreenStyle</item>
|
||||||
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
|
<item name="lockscreen_key_button_theme">@style/PinCodeKeyButtonStyle</item>
|
||||||
<item name="pf_title">@style/PinCodeTitleStyle</item>
|
<item name="lockscreen_title_theme">@style/PinCodeTitleStyle</item>
|
||||||
<item name="pf_hint">@style/PinCodeHintStyle</item>
|
<item name="lockscreen_hint_theme">@style/PinCodeHintStyle</item>
|
||||||
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
|
<item name="lockscreen_code_view_theme">@style/PinCodeDotsViewStyle</item>
|
||||||
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
|
<item name="lockscreen_delete_button_theme">@style/PinCodeDeleteButtonStyle</item>
|
||||||
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
|
<item name="lockscreen_fingerprint_button_theme">@style/PinCodeFingerprintButtonStyle</item>
|
||||||
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
|
<item name="lockscreen_next_theme">@style/PinCodeNextButtonStyle</item>
|
||||||
|
|
||||||
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>
|
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>
|
||||||
<item name="android:navigationBarColor">@color/android_navigation_bar_background_dark</item>
|
<item name="android:navigationBarColor">@color/android_navigation_bar_background_dark</item>
|
||||||
|
|
|
@ -111,14 +111,14 @@
|
||||||
|
|
||||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||||
|
|
||||||
<item name="pf_lock_screen">@style/PinCodeScreenStyle</item>
|
<item name="lockscreen_theme">@style/PinCodeScreenStyle</item>
|
||||||
<item name="pf_key_button">@style/PinCodeKeyButtonStyle</item>
|
<item name="lockscreen_key_button_theme">@style/PinCodeKeyButtonStyle</item>
|
||||||
<item name="pf_title">@style/PinCodeTitleStyle</item>
|
<item name="lockscreen_title_theme">@style/PinCodeTitleStyle</item>
|
||||||
<item name="pf_hint">@style/PinCodeHintStyle</item>
|
<item name="lockscreen_hint_theme">@style/PinCodeHintStyle</item>
|
||||||
<item name="pf_code_view">@style/PinCodeDotsViewStyle</item>
|
<item name="lockscreen_code_view_theme">@style/PinCodeDotsViewStyle</item>
|
||||||
<item name="pf_delete_button">@style/PinCodeDeleteButtonStyle</item>
|
<item name="lockscreen_delete_button_theme">@style/PinCodeDeleteButtonStyle</item>
|
||||||
<item name="pf_fingerprint_button">@style/PinCodeFingerprintButtonStyle</item>
|
<item name="lockscreen_fingerprint_button_theme">@style/PinCodeFingerprintButtonStyle</item>
|
||||||
<item name="pf_next">@style/PinCodeNextButtonStyle</item>
|
<item name="lockscreen_next_theme">@style/PinCodeNextButtonStyle</item>
|
||||||
|
|
||||||
<!-- Use dark color, to have enough contrast with icons color. windowLightStatusBar is only available in API 23+ -->
|
<!-- Use dark color, to have enough contrast with icons color. windowLightStatusBar is only available in API 23+ -->
|
||||||
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>
|
<item name="android:statusBarColor">@color/android_status_bar_background_dark</item>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.securestorage
|
package org.matrix.android.sdk
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
|
||||||
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
|
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
|
||||||
var value: Int = 0
|
var value: Int = 0
|
|
@ -14,40 +14,57 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.securestorage
|
package org.matrix.android.sdk.api.securestorage
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import io.mockk.clearAllMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.spyk
|
||||||
|
import org.amshove.kluent.invoking
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldBeInstanceOf
|
||||||
|
import org.amshove.kluent.shouldNotThrow
|
||||||
|
import org.amshove.kluent.shouldThrow
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.TestBuildVersionSdkIntProvider
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.KeyStoreException
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
class SecretStoringUtilsTest : InstrumentedTest {
|
class SecretStoringUtilsTest {
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
|
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
|
||||||
private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider)
|
private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) }
|
||||||
|
private val secretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TEST_STR = "This is something I want to store safely!"
|
const val TEST_STR = "This is something I want to store safely!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
clearAllMocks()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testStringNominalCaseApi21() {
|
fun testStringNominalCaseApi21() {
|
||||||
val alias = generateAlias()
|
val alias = generateAlias()
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
// Encrypt
|
// Encrypt
|
||||||
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
|
val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
|
||||||
// Decrypt
|
// Decrypt
|
||||||
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
|
val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
|
||||||
decrypted shouldBeEqualTo TEST_STR
|
decrypted shouldBeEqualTo TEST_STR
|
||||||
secretStoringUtils.safeDeleteKey(alias)
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
}
|
}
|
||||||
|
@ -57,9 +74,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
|
||||||
val alias = generateAlias()
|
val alias = generateAlias()
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
// Encrypt
|
// Encrypt
|
||||||
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
|
val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
|
||||||
// Decrypt
|
// Decrypt
|
||||||
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
|
val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
|
||||||
decrypted shouldBeEqualTo TEST_STR
|
decrypted shouldBeEqualTo TEST_STR
|
||||||
secretStoringUtils.safeDeleteKey(alias)
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
}
|
}
|
||||||
|
@ -69,9 +86,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
|
||||||
val alias = generateAlias()
|
val alias = generateAlias()
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
|
||||||
// Encrypt
|
// Encrypt
|
||||||
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
|
val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
|
||||||
// Decrypt
|
// Decrypt
|
||||||
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
|
val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
|
||||||
decrypted shouldBeEqualTo TEST_STR
|
decrypted shouldBeEqualTo TEST_STR
|
||||||
secretStoringUtils.safeDeleteKey(alias)
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
}
|
}
|
||||||
|
@ -81,13 +98,13 @@ class SecretStoringUtilsTest : InstrumentedTest {
|
||||||
val alias = generateAlias()
|
val alias = generateAlias()
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
// Encrypt
|
// Encrypt
|
||||||
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
|
val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
|
||||||
|
|
||||||
// Simulate a system upgrade
|
// Simulate a system upgrade
|
||||||
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
|
|
||||||
// Decrypt
|
// Decrypt
|
||||||
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
|
val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
|
||||||
decrypted shouldBeEqualTo TEST_STR
|
decrypted shouldBeEqualTo TEST_STR
|
||||||
secretStoringUtils.safeDeleteKey(alias)
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
}
|
}
|
||||||
|
@ -180,5 +197,56 @@ class SecretStoringUtilsTest : InstrumentedTest {
|
||||||
secretStoringUtils.safeDeleteKey(alias)
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEnsureKeyReturnsSymmetricKeyOnAndroidM() {
|
||||||
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
|
val alias = generateAlias()
|
||||||
|
|
||||||
|
val key = secretStoringUtils.ensureKey(alias)
|
||||||
|
key shouldBeInstanceOf KeyStore.SecretKeyEntry::class
|
||||||
|
|
||||||
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEnsureKeyReturnsPrivateKeyOnAndroidL() {
|
||||||
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
|
val alias = generateAlias()
|
||||||
|
|
||||||
|
val key = secretStoringUtils.ensureKey(alias)
|
||||||
|
key shouldBeInstanceOf KeyStore.PrivateKeyEntry::class
|
||||||
|
|
||||||
|
secretStoringUtils.safeDeleteKey(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSafeDeleteCanHandleKeyStoreExceptions() {
|
||||||
|
every { keyStore.deleteEntry(any()) } throws KeyStoreException()
|
||||||
|
|
||||||
|
invoking { secretStoringUtils.safeDeleteKey(generateAlias()) } shouldNotThrow KeyStoreException::class
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLoadSecureSecretBytesWillThrowOnInvalidStreamFormat() {
|
||||||
|
invoking {
|
||||||
|
secretStoringUtils.loadSecureSecretBytes(byteArrayOf(255.toByte()), generateAlias())
|
||||||
|
} shouldThrow IllegalArgumentException::class
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLoadSecureSecretWillThrowOnInvalidStreamFormat() {
|
||||||
|
invoking {
|
||||||
|
secretStoringUtils.loadSecureSecret(byteArrayOf(255.toByte()).inputStream(), generateAlias())
|
||||||
|
} shouldThrow IllegalArgumentException::class
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateAlias() = UUID.randomUUID().toString()
|
private fun generateAlias() = UUID.randomUUID().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.toBase64NoPadding(): String {
|
||||||
|
return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.fromBase64(): ByteArray {
|
||||||
|
return Base64.decode(this, Base64.DEFAULT)
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageModule
|
||||||
import org.matrix.android.sdk.internal.auth.AuthModule
|
import org.matrix.android.sdk.internal.auth.AuthModule
|
||||||
import org.matrix.android.sdk.internal.debug.DebugModule
|
import org.matrix.android.sdk.internal.debug.DebugModule
|
||||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||||
|
@ -39,7 +40,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
|
||||||
RawModule::class,
|
RawModule::class,
|
||||||
DebugModule::class,
|
DebugModule::class,
|
||||||
SettingsModule::class,
|
SettingsModule::class,
|
||||||
SystemModule::class
|
SystemModule::class,
|
||||||
|
SecureStorageModule::class,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
|
@ -51,7 +53,7 @@ internal interface TestMatrixComponent : MatrixComponent {
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
@BindsInstance context: Context,
|
@BindsInstance context: Context,
|
||||||
@BindsInstance matrixConfiguration: MatrixConfiguration
|
@BindsInstance matrixConfiguration: MatrixConfiguration,
|
||||||
): TestMatrixComponent
|
): TestMatrixComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package org.matrix.android.sdk.api
|
package org.matrix.android.sdk.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -30,6 +32,7 @@ import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
||||||
import org.matrix.android.sdk.api.network.ApiInterceptorListener
|
import org.matrix.android.sdk.api.network.ApiInterceptorListener
|
||||||
import org.matrix.android.sdk.api.network.ApiPath
|
import org.matrix.android.sdk.api.network.ApiPath
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageService
|
||||||
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
|
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
|
||||||
|
@ -64,6 +67,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
@Inject internal lateinit var apiInterceptor: ApiInterceptor
|
@Inject internal lateinit var apiInterceptor: ApiInterceptor
|
||||||
@Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory
|
@Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory
|
||||||
@Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage
|
@Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage
|
||||||
|
@Inject internal lateinit var secureStorageService: SecureStorageService
|
||||||
|
|
||||||
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val appContext = context.applicationContext
|
val appContext = context.applicationContext
|
||||||
|
@ -76,7 +82,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
.build()
|
.build()
|
||||||
WorkManager.initialize(appContext, configuration)
|
WorkManager.initialize(appContext, configuration)
|
||||||
}
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
uiHandler.post {
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,6 +123,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
*/
|
*/
|
||||||
fun legacySessionImporter() = legacySessionImporter
|
fun legacySessionImporter() = legacySessionImporter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SecureStorageService used to encrypt and decrypt sensitive data.
|
||||||
|
*/
|
||||||
|
fun secureStorageService(): SecureStorageService = secureStorageService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the worker factory. The returned value has to be provided to `WorkConfiguration.Builder()`.
|
* Get the worker factory. The returned value has to be provided to `WorkConfiguration.Builder()`.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
@file:Suppress("DEPRECATION")
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.securestorage
|
package org.matrix.android.sdk.api.securestorage
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -25,7 +25,7 @@ import android.security.KeyPairGeneratorSpec
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
@ -80,9 +80,11 @@ import javax.security.auth.x500.X500Principal
|
||||||
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
|
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
|
||||||
* add a pin or change the schema); So you might and with a useless pile of bytes.
|
* add a pin or change the schema); So you might and with a useless pile of bytes.
|
||||||
*/
|
*/
|
||||||
internal class SecretStoringUtils @Inject constructor(
|
class SecretStoringUtils @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider
|
private val keyStore: KeyStore,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
private val keyNeedsUserAuthentication: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -94,14 +96,24 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
private const val FORMAT_1: Byte = 1
|
private const val FORMAT_1: Byte = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keyStore: KeyStore by lazy {
|
|
||||||
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
|
||||||
load(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val secureRandom = SecureRandom()
|
private val secureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows creation of the crypto keys associated witht he [alias] before encrypting some value with it.
|
||||||
|
* @return A [KeyStore.Entry] with the keys.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun ensureKey(alias: String): KeyStore.Entry {
|
||||||
|
when {
|
||||||
|
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> getOrGenerateSymmetricKeyForAliasM(alias)
|
||||||
|
else -> getOrGenerateKeyPairForAlias(alias).privateKey
|
||||||
|
}
|
||||||
|
return keyStore.getEntry(alias, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the key associated with the [keyAlias] and logs any [KeyStoreException] that could happen.
|
||||||
|
*/
|
||||||
fun safeDeleteKey(keyAlias: String) {
|
fun safeDeleteKey(keyAlias: String) {
|
||||||
try {
|
try {
|
||||||
keyStore.deleteEntry(keyAlias)
|
keyStore.deleteEntry(keyAlias)
|
||||||
|
@ -121,24 +133,24 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun securelyStoreString(secret: String, keyAlias: String): ByteArray {
|
fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
|
||||||
return when {
|
return when {
|
||||||
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
|
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias)
|
||||||
else -> encryptString(secret, keyAlias)
|
else -> encryptBytes(secret, keyAlias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a secret that was encrypted by #securelyStoreString().
|
* Decrypt a secret that was encrypted by [securelyStoreBytes].
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String {
|
fun loadSecureSecretBytes(encrypted: ByteArray, keyAlias: String): ByteArray {
|
||||||
encrypted.inputStream().use { inputStream ->
|
encrypted.inputStream().use { inputStream ->
|
||||||
// First get the format
|
// First get the format
|
||||||
return when (val format = inputStream.read().toByte()) {
|
return when (val format = inputStream.read().toByte()) {
|
||||||
FORMAT_API_M -> decryptStringM(inputStream, keyAlias)
|
FORMAT_API_M -> decryptBytesM(inputStream, keyAlias)
|
||||||
FORMAT_1 -> decryptString(inputStream, keyAlias)
|
FORMAT_1 -> decryptBytes(inputStream, keyAlias)
|
||||||
else -> throw IllegalArgumentException("Unknown format $format")
|
else -> throw IllegalArgumentException("Unknown format $format")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,6 +174,22 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEncryptCipher(alias: String): Cipher {
|
||||||
|
val key = when (val keyEntry = ensureKey(alias)) {
|
||||||
|
is KeyStore.SecretKeyEntry -> keyEntry.secretKey
|
||||||
|
is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
|
||||||
|
else -> throw IllegalStateException("Unknown KeyEntry type.")
|
||||||
|
}
|
||||||
|
val cipherMode = when {
|
||||||
|
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
|
||||||
|
else -> RSA_MODE
|
||||||
|
}
|
||||||
|
val cipher = Cipher.getInstance(cipherMode)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||||
|
return cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
|
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
|
||||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||||
|
@ -176,6 +204,13 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
.setKeySize(128)
|
.setKeySize(128)
|
||||||
|
.apply {
|
||||||
|
setUserAuthenticationRequired(keyNeedsUserAuthentication)
|
||||||
|
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) {
|
||||||
|
setInvalidatedByBiometricEnrollment(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setUserAuthenticationRequired(keyNeedsUserAuthentication)
|
||||||
.build()
|
.build()
|
||||||
generator.init(keyGenSpec)
|
generator.init(keyGenSpec)
|
||||||
return generator.generateKey()
|
return generator.generateKey()
|
||||||
|
@ -216,19 +251,16 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun encryptStringM(text: String, keyAlias: String): ByteArray {
|
private fun encryptBytesM(byteArray: ByteArray, keyAlias: String): ByteArray {
|
||||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
val cipher = getEncryptCipher(keyAlias)
|
||||||
|
|
||||||
val cipher = Cipher.getInstance(AES_MODE)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
|
||||||
val iv = cipher.iv
|
val iv = cipher.iv
|
||||||
// we happen the iv to the final result
|
// we happen the iv to the final result
|
||||||
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
|
||||||
return formatMMake(iv, encryptedBytes)
|
return formatMMake(iv, encryptedBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun decryptStringM(inputStream: InputStream, keyAlias: String): String {
|
private fun decryptBytesM(inputStream: InputStream, keyAlias: String): ByteArray {
|
||||||
val (iv, encryptedText) = formatMExtract(inputStream)
|
val (iv, encryptedText) = formatMExtract(inputStream)
|
||||||
|
|
||||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
||||||
|
@ -237,10 +269,10 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
val spec = GCMParameterSpec(128, iv)
|
val spec = GCMParameterSpec(128, iv)
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||||
|
|
||||||
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
|
return cipher.doFinal(encryptedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encryptString(text: String, keyAlias: String): ByteArray {
|
private fun encryptBytes(byteArray: ByteArray, keyAlias: String): ByteArray {
|
||||||
// we generate a random symmetric key
|
// we generate a random symmetric key
|
||||||
val key = ByteArray(16)
|
val key = ByteArray(16)
|
||||||
secureRandom.nextBytes(key)
|
secureRandom.nextBytes(key)
|
||||||
|
@ -252,12 +284,12 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
val cipher = Cipher.getInstance(AES_MODE)
|
val cipher = Cipher.getInstance(AES_MODE)
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||||
val iv = cipher.iv
|
val iv = cipher.iv
|
||||||
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
|
val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
|
||||||
|
|
||||||
return format1Make(encryptedKey, iv, encryptedBytes)
|
return format1Make(encryptedKey, iv, encryptedBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptString(inputStream: InputStream, keyAlias: String): String {
|
private fun decryptBytes(inputStream: InputStream, keyAlias: String): ByteArray {
|
||||||
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
|
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
|
||||||
|
|
||||||
// we need to decrypt the key
|
// we need to decrypt the key
|
||||||
|
@ -266,16 +298,13 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
val spec = GCMParameterSpec(128, iv)
|
val spec = GCMParameterSpec(128, iv)
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
||||||
|
|
||||||
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
|
return cipher.doFinal(encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
|
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
val cipher = getEncryptCipher(keyAlias)
|
||||||
|
|
||||||
val cipher = Cipher.getInstance(AES_MODE)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
|
|
||||||
val iv = cipher.iv
|
val iv = cipher.iv
|
||||||
|
|
||||||
val bos1 = ByteArrayOutputStream()
|
val bos1 = ByteArrayOutputStream()
|
||||||
|
@ -362,10 +391,8 @@ internal class SecretStoringUtils @Inject constructor(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
|
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
|
||||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
|
|
||||||
// Encrypt the text
|
// Encrypt the text
|
||||||
val inputCipher = Cipher.getInstance(RSA_MODE)
|
val inputCipher = getEncryptCipher(alias)
|
||||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
CipherOutputStream(outputStream, inputCipher).use {
|
CipherOutputStream(outputStream, inputCipher).use {
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.securestorage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
|
||||||
|
import java.security.KeyStore
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class SecureStorageModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideSecretStoringUtils(
|
||||||
|
context: Context,
|
||||||
|
keyStore: KeyStore,
|
||||||
|
buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
): SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.securestorage
|
package org.matrix.android.sdk.api.securestorage
|
||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
|
@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomService
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
import org.matrix.android.sdk.api.session.search.SearchService
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
import org.matrix.android.sdk.api.session.space.SpaceService
|
import org.matrix.android.sdk.api.session.space.SpaceService
|
||||||
|
@ -200,11 +199,6 @@ interface Session {
|
||||||
*/
|
*/
|
||||||
fun syncService(): SyncService
|
fun syncService(): SyncService
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the SecureStorageService associated with the session.
|
|
||||||
*/
|
|
||||||
fun secureStorageService(): SecureStorageService
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ProfileService associated with the session.
|
* Returns the ProfileService associated with the session.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -14,9 +14,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.util.system
|
package org.matrix.android.sdk.api.util
|
||||||
|
|
||||||
internal interface BuildVersionSdkIntProvider {
|
interface BuildVersionSdkIntProvider {
|
||||||
/**
|
/**
|
||||||
* Return the current version of the Android SDK.
|
* Return the current version of the Android SDK.
|
||||||
*/
|
*/
|
|
@ -14,12 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.util.system
|
package org.matrix.android.sdk.api.util
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultBuildVersionSdkIntProvider @Inject constructor() :
|
class DefaultBuildVersionSdkIntProvider @Inject constructor() :
|
||||||
BuildVersionSdkIntProvider {
|
BuildVersionSdkIntProvider {
|
||||||
override fun get() = Build.VERSION.SDK_INT
|
override fun get() = Build.VERSION.SDK_INT
|
||||||
}
|
}
|
|
@ -21,7 +21,7 @@ import androidx.core.content.edit
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import org.matrix.android.sdk.BuildConfig
|
import org.matrix.android.sdk.BuildConfig
|
||||||
import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -40,7 +40,7 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
internal class RealmKeysUtils @Inject constructor(
|
internal class RealmKeysUtils @Inject constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val secretStoringUtils: SecretStoringUtils
|
private val secretStoringUtils: SecretStoringUtils,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val rng = SecureRandom()
|
private val rng = SecureRandom()
|
||||||
|
@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(
|
||||||
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
|
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
|
||||||
val key = generateKeyForRealm()
|
val key = generateKeyForRealm()
|
||||||
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
|
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
|
||||||
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
|
val toStore = secretStoringUtils.securelyStoreBytes(encodedKey.toByteArray(), alias)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
|
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ internal class RealmKeysUtils @Inject constructor(
|
||||||
private fun extractKeyForDatabase(alias: String): ByteArray {
|
private fun extractKeyForDatabase(alias: String): ByteArray {
|
||||||
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
|
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
|
||||||
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
|
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
|
||||||
val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
|
val b64 = secretStoringUtils.loadSecureSecretBytes(encryptedKey, alias)
|
||||||
return Base64.decode(b64, Base64.NO_PADDING)
|
return Base64.decode(b64, Base64.NO_PADDING)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageModule
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageService
|
||||||
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import org.matrix.android.sdk.internal.auth.AuthModule
|
import org.matrix.android.sdk.internal.auth.AuthModule
|
||||||
|
@ -53,7 +55,8 @@ import java.io.File
|
||||||
DebugModule::class,
|
DebugModule::class,
|
||||||
SettingsModule::class,
|
SettingsModule::class,
|
||||||
SystemModule::class,
|
SystemModule::class,
|
||||||
NoOpTestModule::class
|
NoOpTestModule::class,
|
||||||
|
SecureStorageModule::class,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MatrixScope
|
@MatrixScope
|
||||||
|
@ -96,6 +99,8 @@ internal interface MatrixComponent {
|
||||||
|
|
||||||
fun sessionManager(): SessionManager
|
fun sessionManager(): SessionManager
|
||||||
|
|
||||||
|
fun secureStorageService(): SecureStorageService
|
||||||
|
|
||||||
fun matrixWorkerFactory(): MatrixWorkerFactory
|
fun matrixWorkerFactory(): MatrixWorkerFactory
|
||||||
|
|
||||||
fun inject(matrix: Matrix)
|
fun inject(matrix: Matrix)
|
||||||
|
|
|
@ -14,9 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.securestorage
|
package org.matrix.android.sdk.internal.securestorage
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageService
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
|
@ -55,7 +55,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomService
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
import org.matrix.android.sdk.api.session.search.SearchService
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
import org.matrix.android.sdk.api.session.space.SpaceService
|
import org.matrix.android.sdk.api.session.space.SpaceService
|
||||||
|
@ -111,7 +110,6 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val cryptoService: Lazy<DefaultCryptoService>,
|
private val cryptoService: Lazy<DefaultCryptoService>,
|
||||||
private val defaultFileService: Lazy<FileService>,
|
private val defaultFileService: Lazy<FileService>,
|
||||||
private val permalinkService: Lazy<PermalinkService>,
|
private val permalinkService: Lazy<PermalinkService>,
|
||||||
private val secureStorageService: Lazy<SecureStorageService>,
|
|
||||||
private val profileService: Lazy<ProfileService>,
|
private val profileService: Lazy<ProfileService>,
|
||||||
private val syncService: Lazy<SyncService>,
|
private val syncService: Lazy<SyncService>,
|
||||||
private val mediaService: Lazy<MediaService>,
|
private val mediaService: Lazy<MediaService>,
|
||||||
|
@ -220,7 +218,6 @@ internal class DefaultSession @Inject constructor(
|
||||||
override fun eventService(): EventService = eventService.get()
|
override fun eventService(): EventService = eventService.get()
|
||||||
override fun termsService(): TermsService = termsService.get()
|
override fun termsService(): TermsService = termsService.get()
|
||||||
override fun syncService(): SyncService = syncService.get()
|
override fun syncService(): SyncService = syncService.get()
|
||||||
override fun secureStorageService(): SecureStorageService = secureStorageService.get()
|
|
||||||
override fun profileService(): ProfileService = profileService.get()
|
override fun profileService(): ProfileService = profileService.get()
|
||||||
override fun presenceService(): PresenceService = presenceService.get()
|
override fun presenceService(): PresenceService = presenceService.get()
|
||||||
override fun accountService(): AccountService = accountService.get()
|
override fun accountService(): AccountService = accountService.get()
|
||||||
|
|
|
@ -20,6 +20,7 @@ import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageModule
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||||
|
@ -98,7 +99,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
|
||||||
ThirdPartyModule::class,
|
ThirdPartyModule::class,
|
||||||
SpaceModule::class,
|
SpaceModule::class,
|
||||||
PresenceModule::class,
|
PresenceModule::class,
|
||||||
RequestModule::class
|
RequestModule::class,
|
||||||
|
SecureStorageModule::class,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.events.EventService
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||||
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.api.util.md5
|
import org.matrix.android.sdk.api.util.md5
|
||||||
|
@ -93,7 +92,6 @@ import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcesso
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
|
||||||
import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor
|
import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService
|
|
||||||
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
|
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
|
||||||
import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService
|
import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService
|
||||||
import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter
|
import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter
|
||||||
|
@ -367,9 +365,6 @@ internal abstract class SessionModule {
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver
|
abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
|
abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.util.system
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecureStorageService
|
||||||
|
import org.matrix.android.sdk.internal.securestorage.DefaultSecureStorageService
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ import org.matrix.android.sdk.internal.util.time.DefaultClock
|
||||||
internal abstract class SystemModule {
|
internal abstract class SystemModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
|
abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindClock(clock: DefaultClock): Clock
|
abstract fun bindClock(clock: DefaultClock): Clock
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.util
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
|
||||||
|
|
||||||
|
class DefaultBuildVersionSdkIntProviderTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getReturnsCurrentVersionFromBuild_Version_SDK_INT() {
|
||||||
|
val provider = DefaultBuildVersionSdkIntProvider()
|
||||||
|
provider.get() shouldBeEqualTo Build.VERSION.SDK_INT
|
||||||
|
}
|
||||||
|
}
|
|
@ -361,6 +361,7 @@ dependencies {
|
||||||
implementation libs.androidx.core
|
implementation libs.androidx.core
|
||||||
implementation "androidx.media:media:1.6.0"
|
implementation "androidx.media:media:1.6.0"
|
||||||
implementation "androidx.transition:transition:1.4.1"
|
implementation "androidx.transition:transition:1.4.1"
|
||||||
|
implementation libs.androidx.biometric
|
||||||
|
|
||||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
|
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
|
||||||
|
@ -421,7 +422,6 @@ dependencies {
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation libs.androidx.autoFill
|
implementation libs.androidx.autoFill
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
|
||||||
implementation 'com.github.hyuwah:DraggableView:1.0.0'
|
implementation 'com.github.hyuwah:DraggableView:1.0.0'
|
||||||
|
|
||||||
// Custom Tab
|
// Custom Tab
|
||||||
|
@ -561,4 +561,5 @@ dependencies {
|
||||||
}
|
}
|
||||||
androidTestImplementation libs.mockk.mockkAndroid
|
androidTestImplementation libs.mockk.mockkAndroid
|
||||||
androidTestUtil libs.androidx.orchestrator
|
androidTestUtil libs.androidx.orchestrator
|
||||||
|
debugImplementation libs.androidx.fragmentTesting
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
import org.hamcrest.CoreMatchers.not
|
import org.hamcrest.CoreMatchers.not
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
|
@ -106,6 +107,12 @@ class RegistrationTest {
|
||||||
.check(matches(isEnabled()))
|
.check(matches(isEnabled()))
|
||||||
.perform(closeSoftKeyboard(), click())
|
.perform(closeSoftKeyboard(), click())
|
||||||
|
|
||||||
|
withIdlingResource(activityIdlingResource(AnalyticsOptInActivity::class.java)) {
|
||||||
|
onView(withId(R.id.later))
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(click())
|
||||||
|
}
|
||||||
|
|
||||||
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
withIdlingResource(activityIdlingResource(HomeActivity::class.java)) {
|
||||||
onView(withId(R.id.roomListContainer))
|
onView(withId(R.id.roomListContainer))
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
|
||||||
|
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
|
||||||
|
var value: Int = 0
|
||||||
|
|
||||||
|
override fun get() = value
|
||||||
|
}
|
|
@ -25,9 +25,11 @@ import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StrikethroughSpan
|
import android.text.style.StrikethroughSpan
|
||||||
import android.text.style.UnderlineSpan
|
import android.text.style.UnderlineSpan
|
||||||
import androidx.emoji2.text.EmojiCompat
|
import androidx.emoji2.text.EmojiCompat
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import im.vector.app.InstrumentedTest
|
import im.vector.app.InstrumentedTest
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.amshove.kluent.shouldBeTrue
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.BeforeClass
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -42,6 +44,14 @@ import java.util.concurrent.TimeUnit
|
||||||
@Ignore
|
@Ignore
|
||||||
class SpanUtilsTest : InstrumentedTest {
|
class SpanUtilsTest : InstrumentedTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeClass
|
||||||
|
@JvmStatic
|
||||||
|
fun setupClass() {
|
||||||
|
EmojiCompat.init(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val spanUtils = SpanUtils {
|
private val spanUtils = SpanUtils {
|
||||||
val emojiCompat = EmojiCompat.get()
|
val emojiCompat = EmojiCompat.get()
|
||||||
emojiCompat.waitForInit()
|
emojiCompat.waitForInit()
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen
|
||||||
|
|
||||||
|
object LockScreenTestConstants {
|
||||||
|
const val ALIAS = "some_alias"
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.biometrics
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.TestBuildVersionSdkIntProvider
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
|
import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
|
||||||
|
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
||||||
|
import io.mockk.clearAllMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.unmockkObject
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BiometricHelperTests {
|
||||||
|
|
||||||
|
private val biometricManager = mockk<BiometricManager>(relaxed = true)
|
||||||
|
private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true)
|
||||||
|
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
clearAllMocks()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun canUseWeakBiometricAuthReturnsTrueIfIsFaceUnlockEnabledAndCanAuthenticate() {
|
||||||
|
every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_SUCCESS
|
||||||
|
val configuration = createDefaultConfiguration(isFaceUnlockEnabled = true)
|
||||||
|
val biometricUtils = createBiometricHelper(configuration)
|
||||||
|
|
||||||
|
biometricUtils.canUseWeakBiometricAuth.shouldBeTrue()
|
||||||
|
|
||||||
|
val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isFaceUnlockEnabled = false))
|
||||||
|
biometricUtilsWithDisabledAuth.canUseWeakBiometricAuth.shouldBeFalse()
|
||||||
|
|
||||||
|
every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
biometricUtils.canUseWeakBiometricAuth.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun canUseStrongBiometricAuthReturnsTrueIfIsBiometricsEnabledAndCanAuthenticate() {
|
||||||
|
every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_SUCCESS
|
||||||
|
val configuration = createDefaultConfiguration(isBiometricsEnabled = true)
|
||||||
|
val biometricUtils = createBiometricHelper(configuration)
|
||||||
|
|
||||||
|
biometricUtils.canUseStrongBiometricAuth.shouldBeTrue()
|
||||||
|
|
||||||
|
val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = false))
|
||||||
|
biometricUtilsWithDisabledAuth.canUseStrongBiometricAuth.shouldBeFalse()
|
||||||
|
|
||||||
|
every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
biometricUtils.canUseStrongBiometricAuth.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun canUseDeviceCredentialAuthReturnsTrueIfIsDeviceCredentialsUnlockEnabledAndCanAuthenticate() {
|
||||||
|
every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_SUCCESS
|
||||||
|
val configuration = createDefaultConfiguration(isDeviceCredentialUnlockEnabled = true)
|
||||||
|
val biometricUtils = createBiometricHelper(configuration)
|
||||||
|
|
||||||
|
biometricUtils.canUseDeviceCredentialsAuth.shouldBeTrue()
|
||||||
|
|
||||||
|
val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isDeviceCredentialUnlockEnabled = false))
|
||||||
|
biometricUtilsWithDisabledAuth.canUseDeviceCredentialsAuth.shouldBeFalse()
|
||||||
|
|
||||||
|
every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
biometricUtils.canUseDeviceCredentialsAuth.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isSystemAuthEnabledReturnsTrueIfAnyAuthenticationMethodIsAvailableAndEnabledAndSystemKeyExists() {
|
||||||
|
val biometricHelper = mockk<BiometricHelper>(relaxed = true) {
|
||||||
|
every { hasSystemKey } returns true
|
||||||
|
every { isSystemKeyValid } returns true
|
||||||
|
every { canUseAnySystemAuth } answers { callOriginal() }
|
||||||
|
every { isSystemAuthEnabledAndValid } answers { callOriginal() }
|
||||||
|
}
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse()
|
||||||
|
|
||||||
|
every { biometricHelper.canUseWeakBiometricAuth } returns true
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
|
||||||
|
|
||||||
|
every { biometricHelper.canUseWeakBiometricAuth } returns false
|
||||||
|
every { biometricHelper.canUseStrongBiometricAuth } returns true
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
|
||||||
|
|
||||||
|
every { biometricHelper.canUseStrongBiometricAuth } returns false
|
||||||
|
every { biometricHelper.canUseDeviceCredentialsAuth } returns true
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue()
|
||||||
|
|
||||||
|
every { biometricHelper.isSystemKeyValid } returns false
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasSystemKeyReturnsKeyHelperHasSystemKey() {
|
||||||
|
val biometricUtils = createBiometricHelper(createDefaultConfiguration())
|
||||||
|
every { lockScreenKeyRepository.hasSystemKey() } returns true
|
||||||
|
biometricUtils.hasSystemKey.shouldBeTrue()
|
||||||
|
|
||||||
|
every { lockScreenKeyRepository.hasSystemKey() } returns false
|
||||||
|
biometricUtils.hasSystemKey.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isSystemKeyValidReturnsKeyHelperIsSystemKeyValid() {
|
||||||
|
val biometricUtils = createBiometricHelper(createDefaultConfiguration())
|
||||||
|
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
|
||||||
|
biometricUtils.isSystemKeyValid.shouldBeTrue()
|
||||||
|
|
||||||
|
every { lockScreenKeyRepository.isSystemKeyValid() } returns false
|
||||||
|
biometricUtils.isSystemKeyValid.shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disableAuthenticationDeletesSystemKeyAndCancelsPrompt() {
|
||||||
|
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration()))
|
||||||
|
biometricUtils.disableAuthentication()
|
||||||
|
|
||||||
|
verify { lockScreenKeyRepository.deleteSystemKey() }
|
||||||
|
verify { biometricUtils.cancelPrompt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Ignore("This won't work in CI as the emulator won't have biometric auth enabled.")
|
||||||
|
@Test
|
||||||
|
fun authenticateShowsPrompt() = runTest {
|
||||||
|
val biometricUtils = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))
|
||||||
|
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
with(ActivityScenario.launch(LockScreenTestActivity::class.java)) {
|
||||||
|
onActivity { activity ->
|
||||||
|
biometricUtils.authenticate(activity)
|
||||||
|
activity.supportFragmentManager.fragments.isNotEmpty().shouldBeTrue()
|
||||||
|
close()
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latch.await(1, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@Test
|
||||||
|
fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest {
|
||||||
|
mockkStatic("kotlinx.coroutines.flow.FlowKt")
|
||||||
|
val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) {
|
||||||
|
// Empty flow to keep the dialog open
|
||||||
|
every { receiveAsFlow() } returns flowOf()
|
||||||
|
}
|
||||||
|
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
|
||||||
|
every { createAuthChannel() } returns mockAuthChannel
|
||||||
|
}
|
||||||
|
mockkObject(DevicePromptCheck)
|
||||||
|
every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true
|
||||||
|
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
|
||||||
|
with(ActivityScenario.launch<LockScreenTestActivity>(intent)) {
|
||||||
|
onActivity { activity ->
|
||||||
|
biometricUtils.authenticate(activity)
|
||||||
|
launch {
|
||||||
|
activity.supportFragmentManager.fragments.any { it is FallbackBiometricDialogFragment }.shouldBeTrue()
|
||||||
|
close()
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latch.await(1, TimeUnit.SECONDS)
|
||||||
|
unmockkObject(DevicePromptCheck)
|
||||||
|
unmockkStatic("kotlinx.coroutines.flow.FlowKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest {
|
||||||
|
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
|
||||||
|
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
|
||||||
|
val mockAuthChannel = Channel<Boolean>(capacity = 1)
|
||||||
|
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
|
||||||
|
every { createAuthChannel() } returns mockAuthChannel
|
||||||
|
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
|
||||||
|
}
|
||||||
|
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
|
||||||
|
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
launch {
|
||||||
|
mockAuthChannel.send(true)
|
||||||
|
mockAuthChannel.close()
|
||||||
|
}
|
||||||
|
biometricUtils.authenticate(activity).collect()
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await(1, TimeUnit.SECONDS)
|
||||||
|
verify { lockScreenKeyRepository.ensureSystemKey() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val configProvider = LockScreenConfiguratorProvider(configuration)
|
||||||
|
return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDefaultConfiguration(
|
||||||
|
mode: LockScreenMode = LockScreenMode.VERIFY,
|
||||||
|
pinCodeLength: Int = 4,
|
||||||
|
isBiometricsEnabled: Boolean = false,
|
||||||
|
isFaceUnlockEnabled: Boolean = false,
|
||||||
|
isDeviceCredentialUnlockEnabled: Boolean = false,
|
||||||
|
needsNewCodeValidation: Boolean = false,
|
||||||
|
otherChanges: LockScreenConfiguration.() -> LockScreenConfiguration = { this },
|
||||||
|
): LockScreenConfiguration = LockScreenConfiguration(
|
||||||
|
mode,
|
||||||
|
pinCodeLength,
|
||||||
|
isBiometricsEnabled,
|
||||||
|
isFaceUnlockEnabled,
|
||||||
|
isDeviceCredentialUnlockEnabled,
|
||||||
|
needsNewCodeValidation
|
||||||
|
).let(otherChanges)
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.TestBuildVersionSdkIntProvider
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.amshove.kluent.invoking
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.amshove.kluent.shouldThrow
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import java.security.KeyStore
|
||||||
|
|
||||||
|
class KeyStoreCryptoTests {
|
||||||
|
|
||||||
|
private val alias = "some_alias"
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
private val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||||
|
private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
|
||||||
|
private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider))
|
||||||
|
private val keyStoreCrypto = spyk(
|
||||||
|
KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils)
|
||||||
|
)
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun setup() {
|
||||||
|
keyStore.deleteEntry(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ensureKeyChecksValidityOfKeyAndThrows() {
|
||||||
|
keyStore.containsAlias(alias) shouldBe false
|
||||||
|
|
||||||
|
val exception = KeyPermanentlyInvalidatedException()
|
||||||
|
every { secretStoringUtils.getEncryptCipher(any()) } throws exception
|
||||||
|
|
||||||
|
invoking { keyStoreCrypto.ensureKey() } shouldThrow exception
|
||||||
|
keyStoreCrypto.hasValidKey() shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasValidKeyChecksValidityOfKey() {
|
||||||
|
runCatching { keyStoreCrypto.ensureKey() }
|
||||||
|
keyStoreCrypto.hasValidKey() shouldBe true
|
||||||
|
|
||||||
|
val exception = KeyPermanentlyInvalidatedException()
|
||||||
|
every { secretStoringUtils.getEncryptCipher(any()) } throws exception
|
||||||
|
|
||||||
|
runCatching { keyStoreCrypto.ensureKey() }
|
||||||
|
keyStoreCrypto.hasValidKey() shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasKeyChecksIfKeyExists() {
|
||||||
|
keyStoreCrypto.hasKey() shouldBe false
|
||||||
|
|
||||||
|
keyStoreCrypto.ensureKey()
|
||||||
|
keyStoreCrypto.hasKey() shouldBe true
|
||||||
|
keyStore.containsAlias(keyStoreCrypto.alias)
|
||||||
|
|
||||||
|
keyStoreCrypto.deleteKey()
|
||||||
|
keyStoreCrypto.hasKey() shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteKeyRemovesTheKey() {
|
||||||
|
keyStore.containsAlias(alias) shouldBe false
|
||||||
|
|
||||||
|
keyStoreCrypto.ensureKey()
|
||||||
|
keyStore.containsAlias(alias) shouldBe true
|
||||||
|
|
||||||
|
keyStoreCrypto.deleteKey()
|
||||||
|
keyStore.containsAlias(alias) shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkEncryptionAndDecryptionOfStringsWorkAsExpected() {
|
||||||
|
val original = "some plain text"
|
||||||
|
val encryptedString = keyStoreCrypto.encryptToString(original)
|
||||||
|
val encryptedBytes = keyStoreCrypto.encrypt(original)
|
||||||
|
val result = keyStoreCrypto.decryptToString(encryptedString)
|
||||||
|
val resultFromBytes = keyStoreCrypto.decryptToString(encryptedBytes)
|
||||||
|
result shouldBeEqualTo original
|
||||||
|
resultFromBytes shouldBeEqualTo original
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkEncryptionAndDecryptionWorkAsExpected() {
|
||||||
|
val original = "some plain text".toByteArray()
|
||||||
|
val encryptedBytes = keyStoreCrypto.encrypt(original)
|
||||||
|
val encryptedString = keyStoreCrypto.encryptToString(original)
|
||||||
|
val result = keyStoreCrypto.decrypt(encryptedBytes)
|
||||||
|
val resultFromString = keyStoreCrypto.decrypt(encryptedString)
|
||||||
|
result shouldBeEqualTo original
|
||||||
|
resultFromString shouldBeEqualTo original
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasValidKeyReturnsFalseWhenKeyPermanentlyInvalidatedExceptionIsThrown() {
|
||||||
|
every { keyStoreCrypto.hasKey() } returns true
|
||||||
|
every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
|
||||||
|
|
||||||
|
keyStoreCrypto.hasValidKey().shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasValidKeyReturnsFalseWhenKeyDoesNotExist() {
|
||||||
|
every { keyStoreCrypto.hasKey() } returns false
|
||||||
|
keyStoreCrypto.hasValidKey().shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasValidKeyReturnsIfKeyExistsOnAndroidL() {
|
||||||
|
versionProvider.value = Build.VERSION_CODES.LOLLIPOP
|
||||||
|
|
||||||
|
every { keyStoreCrypto.hasKey() } returns true
|
||||||
|
keyStoreCrypto.hasValidKey().shouldBeTrue()
|
||||||
|
|
||||||
|
every { keyStoreCrypto.hasKey() } returns false
|
||||||
|
keyStoreCrypto.hasValidKey().shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getCryptoObjectUsesCipherFromSecretStoringUtils() {
|
||||||
|
keyStoreCrypto.getCryptoObject()
|
||||||
|
verify { secretStoringUtils.getEncryptCipher(any()) }
|
||||||
|
|
||||||
|
every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
|
||||||
|
invoking { keyStoreCrypto.getCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import io.mockk.clearAllMocks
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.coInvoking
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.amshove.kluent.shouldNotThrow
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
|
||||||
|
import java.security.KeyStore
|
||||||
|
|
||||||
|
class LockScreenKeyRepositoryTests {
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
private val buildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider()
|
||||||
|
|
||||||
|
private val keyStoreCryptoFactory: KeyStoreCrypto.Factory = mockk {
|
||||||
|
every { provide(any(), any()) } answers {
|
||||||
|
KeyStoreCrypto(arg(0), false, context, buildVersionSdkIntProvider, keyStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
|
||||||
|
private val pinCodeMigrator: PinCodeMigrator = mockk(relaxed = true)
|
||||||
|
private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val keyStore: KeyStore by lazy {
|
||||||
|
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory))
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
clearAllMocks()
|
||||||
|
keyStore.deleteEntry("base.pin_code")
|
||||||
|
keyStore.deleteEntry("base.system")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ensureSystemKeyCreatesSystemKeyIfNeeded() {
|
||||||
|
lockScreenKeyRepository.ensureSystemKey()
|
||||||
|
lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encryptPinCodeCreatesPinCodeKey() {
|
||||||
|
lockScreenKeyRepository.encryptPinCode("1234")
|
||||||
|
lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decryptPinCodeDecryptsEncodedPinCode() {
|
||||||
|
val decodedPinCode = "1234"
|
||||||
|
val pinCodeKeyCryptoMock = mockk<KeyStoreCrypto>(relaxed = true) {
|
||||||
|
every { decryptToString(any<String>()) } returns decodedPinCode
|
||||||
|
}
|
||||||
|
every { keyStoreCryptoFactory.provide(any(), any()) } returns pinCodeKeyCryptoMock
|
||||||
|
lockScreenKeyRepository.decryptPinCode("SOME_VALUE") shouldBeEqualTo decodedPinCode
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isSystemKeyValidReturnsWhatKeyStoreCryptoHasValidKeyReplies() {
|
||||||
|
val systemKeyCryptoMock = mockk<KeyStoreCrypto>(relaxed = true) {
|
||||||
|
every { hasKey() } returns true
|
||||||
|
}
|
||||||
|
every { keyStoreCryptoFactory.provide(any(), any()) } returns systemKeyCryptoMock
|
||||||
|
|
||||||
|
every { systemKeyCryptoMock.hasValidKey() } returns false
|
||||||
|
lockScreenKeyRepository.isSystemKeyValid().shouldBeFalse()
|
||||||
|
|
||||||
|
every { systemKeyCryptoMock.hasValidKey() } returns true
|
||||||
|
lockScreenKeyRepository.isSystemKeyValid().shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasSystemKeyReturnsTrueAfterSystemKeyIsCreated() {
|
||||||
|
lockScreenKeyRepository.hasSystemKey().shouldBeFalse()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.ensureSystemKey()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasPinCodeKeyReturnsTrueAfterPinCodeKeyIsCreated() {
|
||||||
|
lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.encryptPinCode("1234")
|
||||||
|
|
||||||
|
lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteSystemKeyRemovesTheKeyFromKeyStore() {
|
||||||
|
lockScreenKeyRepository.ensureSystemKey()
|
||||||
|
lockScreenKeyRepository.hasSystemKey().shouldBeTrue()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.deleteSystemKey()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.hasSystemKey().shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deletePinCodeKeyRemovesTheKeyFromKeyStore() {
|
||||||
|
lockScreenKeyRepository.encryptPinCode("1234")
|
||||||
|
lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.deletePinCodeKey()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateKeysIfNeededReturnsEarlyIfNotNeeded() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns false
|
||||||
|
|
||||||
|
lockScreenKeyRepository.migrateKeysIfNeeded()
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { pinCodeMigrator.migrate(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateKeysIfNeededWillMigratePinCodeAndKeys() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns true
|
||||||
|
|
||||||
|
lockScreenKeyRepository.migrateKeysIfNeeded()
|
||||||
|
|
||||||
|
coVerify { pinCodeMigrator.migrate(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateKeysIfNeededWillCreateSystemKeyIfNeeded() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns true
|
||||||
|
every { vectorPreferences.useBiometricsToUnlock() } returns true
|
||||||
|
every { lockScreenKeyRepository.ensureSystemKey() } returns mockk()
|
||||||
|
|
||||||
|
lockScreenKeyRepository.migrateKeysIfNeeded()
|
||||||
|
|
||||||
|
verify { lockScreenKeyRepository.ensureSystemKey() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateKeysIfNeededWillHandleKeyPermanentlyInvalidatedException() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns true
|
||||||
|
every { vectorPreferences.useBiometricsToUnlock() } returns true
|
||||||
|
every { lockScreenKeyRepository.ensureSystemKey() } throws KeyPermanentlyInvalidatedException()
|
||||||
|
|
||||||
|
coInvoking { lockScreenKeyRepository.migrateKeysIfNeeded() } shouldNotThrow KeyPermanentlyInvalidatedException::class
|
||||||
|
|
||||||
|
verify { lockScreenKeyRepository.ensureSystemKey() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.KeyPairGeneratorSpec
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
|
import im.vector.app.features.pin.SharedPrefPinCodeStore
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.spec.MGF1ParameterSpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.OAEPParameterSpec
|
||||||
|
import javax.crypto.spec.PSource
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class PinCodeMigratorTests {
|
||||||
|
|
||||||
|
private val alias = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
private val pinCodeStore: PinCodeStore = spyk(
|
||||||
|
SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context))
|
||||||
|
)
|
||||||
|
private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) }
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk {
|
||||||
|
every { get() } returns Build.VERSION_CODES.M
|
||||||
|
}
|
||||||
|
private val secretStoringUtils: SecretStoringUtils = spyk(
|
||||||
|
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
||||||
|
)
|
||||||
|
private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider))
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) {
|
||||||
|
keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS)
|
||||||
|
}
|
||||||
|
if (keyStore.containsAlias(alias)) {
|
||||||
|
keyStore.deleteEntry(alias)
|
||||||
|
}
|
||||||
|
runBlocking { pinCodeStore.deletePinCode() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isMigrationNeededReturnsTrueIfLegacyKeyExists() {
|
||||||
|
pinCodeMigrator.isMigrationNeeded() shouldBe false
|
||||||
|
|
||||||
|
generateLegacyKey()
|
||||||
|
|
||||||
|
pinCodeMigrator.isMigrationNeeded() shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns false
|
||||||
|
coEvery { pinCodeStore.getPinCode() } returns null
|
||||||
|
|
||||||
|
pinCodeMigrator.migrate(alias)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() }
|
||||||
|
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
|
||||||
|
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
|
||||||
|
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateWillReturnEarlyIfIsNotNeeded() = runTest {
|
||||||
|
every { pinCodeMigrator.isMigrationNeeded() } returns false
|
||||||
|
coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234"
|
||||||
|
every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0)
|
||||||
|
|
||||||
|
pinCodeMigrator.migrate(alias)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() }
|
||||||
|
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
|
||||||
|
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
|
||||||
|
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migratePinCodeM() = runTest {
|
||||||
|
val pinCode = "1234"
|
||||||
|
saveLegacyPinCode(pinCode)
|
||||||
|
|
||||||
|
pinCodeMigrator.migrate(alias)
|
||||||
|
|
||||||
|
coVerify { pinCodeMigrator.getDecryptedPinCode() }
|
||||||
|
verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
|
||||||
|
coVerify { pinCodeStore.savePinCode(any()) }
|
||||||
|
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
|
||||||
|
|
||||||
|
val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias))
|
||||||
|
decodedPinCode shouldBeEqualTo pinCode
|
||||||
|
keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false
|
||||||
|
keyStore.containsAlias(alias) shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migratePinCodeL() = runTest {
|
||||||
|
val pinCode = "1234"
|
||||||
|
every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
|
||||||
|
saveLegacyPinCode(pinCode)
|
||||||
|
|
||||||
|
pinCodeMigrator.migrate(alias)
|
||||||
|
|
||||||
|
coVerify { pinCodeMigrator.getDecryptedPinCode() }
|
||||||
|
verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
|
||||||
|
coVerify { pinCodeStore.savePinCode(any()) }
|
||||||
|
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
|
||||||
|
|
||||||
|
val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias))
|
||||||
|
decodedPinCode shouldBeEqualTo pinCode
|
||||||
|
keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false
|
||||||
|
keyStore.containsAlias(alias) shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateLegacyKey() {
|
||||||
|
if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
|
||||||
|
|
||||||
|
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
||||||
|
generateLegacyKeyM()
|
||||||
|
} else {
|
||||||
|
generateLegacyKeyL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateLegacyKeyL() {
|
||||||
|
val start = Calendar.getInstance()
|
||||||
|
val end = Calendar.getInstance().also { it.add(Calendar.YEAR, 25) }
|
||||||
|
|
||||||
|
val keyGen = KeyPairGenerator
|
||||||
|
.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE)
|
||||||
|
|
||||||
|
val spec = KeyPairGeneratorSpec.Builder(context)
|
||||||
|
.setAlias(LEGACY_PIN_CODE_KEY_ALIAS)
|
||||||
|
.setSubject(X500Principal("CN=$LEGACY_PIN_CODE_KEY_ALIAS"))
|
||||||
|
.setSerialNumber(BigInteger.valueOf(abs(LEGACY_PIN_CODE_KEY_ALIAS.hashCode()).toLong()))
|
||||||
|
.setEndDate(end.time)
|
||||||
|
.setStartDate(start.time)
|
||||||
|
.setSerialNumber(BigInteger.ONE)
|
||||||
|
.setSubject(X500Principal("CN = Secured Preference Store, O = Devliving Online"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
keyGen.initialize(spec)
|
||||||
|
keyGen.generateKeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateLegacyKeyM() {
|
||||||
|
val keyGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE)
|
||||||
|
keyGenerator.initialize(
|
||||||
|
KeyGenParameterSpec.Builder(LEGACY_PIN_CODE_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
keyGenerator.generateKeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveLegacyPinCode(value: String) {
|
||||||
|
generateLegacyKey()
|
||||||
|
val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey
|
||||||
|
val cipher = getLegacyCipher()
|
||||||
|
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
||||||
|
val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded))
|
||||||
|
val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec)
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||||
|
}
|
||||||
|
val bytes = cipher.doFinal(value.toByteArray())
|
||||||
|
val encryptedPinCode = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||||
|
pinCodeStore.savePinCode(encryptedPinCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLegacyCipher(): Cipher {
|
||||||
|
return when (buildVersionSdkIntProvider.get()) {
|
||||||
|
Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL()
|
||||||
|
else -> getCipherM()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCipherL(): Cipher {
|
||||||
|
val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround"
|
||||||
|
val transformation = "RSA/ECB/PKCS1Padding"
|
||||||
|
return Cipher.getInstance(transformation, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCipherM(): Cipher {
|
||||||
|
val transformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
|
||||||
|
return Cipher.getInstance(transformation)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.tests
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
|
class LockScreenTestActivity : FragmentActivity()
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui.fallbackprompt
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.testing.launchFragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.airbnb.mvrx.Mavericks
|
||||||
|
import im.vector.app.R
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
class FallbackBiometricDialogFragmentTests {
|
||||||
|
|
||||||
|
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dismissTriggersOnDismissCallback() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val fragmentScenario = launchFragment<FallbackBiometricDialogFragment>(noArgsBundle())
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
fragment.onDismiss = { latch.countDown() }
|
||||||
|
fragment.dismiss()
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun argsModifyUI() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val args = FallbackBiometricDialogFragment.Args(
|
||||||
|
title = "Title",
|
||||||
|
description = "Description",
|
||||||
|
cancelActionText = "Cancel text",
|
||||||
|
)
|
||||||
|
val fragmentScenario = launchFragment<FallbackBiometricDialogFragment>(bundleOf(Mavericks.KEY_ARG to args))
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
val view = fragment.requireView()
|
||||||
|
view.findViewById<Button>(R.id.cancel_button).text.toString() shouldBeEqualTo args.cancelActionText
|
||||||
|
view.findViewById<TextView>(R.id.fingerprint_description).text.toString() shouldBeEqualTo args.description
|
||||||
|
(fragment as DialogFragment).requireDialog().window?.attributes?.title shouldBeEqualTo args.title
|
||||||
|
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSuccessRendersStateAndDismisses() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val authFlow = MutableSharedFlow<Boolean>(replay = 1)
|
||||||
|
val fragmentScenario = launchFragment<FallbackBiometricDialogFragment>(noArgsBundle())
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.CREATED)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
fragment.onDismiss = { latch.countDown() }
|
||||||
|
fragment.authenticationFlow = authFlow
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
// Espresso wasn't fast enough to catch this value
|
||||||
|
authFlow.tryEmit(true)
|
||||||
|
fragment.requireView().statusText() shouldBeEqualTo context.getString(R.string.lockscreen_fingerprint_success)
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onFailureRendersStateAndResetsItBackAfterDelay() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val authFlow = MutableSharedFlow<Boolean>(replay = 1)
|
||||||
|
val fragmentScenario = launchFragment<FallbackBiometricDialogFragment>(noArgsBundle())
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.CREATED)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
fragment.authenticationFlow = authFlow
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
authFlow.tryEmit(false)
|
||||||
|
fragment.requireView().statusText() shouldBeEqualTo context.getString(R.string.lockscreen_fingerprint_not_recognized)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onErrorDismissesDialog() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
val authChannel = Channel<Boolean>(capacity = 1)
|
||||||
|
val fragmentScenario = launchFragment<FallbackBiometricDialogFragment>(noArgsBundle())
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.CREATED)
|
||||||
|
fragmentScenario.onFragment { fragment ->
|
||||||
|
fragment.onDismiss = { latch.countDown() }
|
||||||
|
fragment.authenticationFlow = authChannel.receiveAsFlow()
|
||||||
|
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
authChannel.close(Exception())
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun noArgsBundle() = bundleOf(Mavericks.KEY_ARG to FallbackBiometricDialogFragment.Args())
|
||||||
|
|
||||||
|
private fun View.statusText(): String = findViewById<TextView>(R.id.fingerprint_status).text.toString()
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.views
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class LockScreenCodeViewTests {
|
||||||
|
|
||||||
|
lateinit var lockScreenCodeView: LockScreenCodeView
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
lockScreenCodeView = LockScreenCodeView(context).apply { codeLength = 4 }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addingCharactersChangesEnteredDigits() {
|
||||||
|
lockScreenCodeView.onCharInput('A')
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCharInputReturnsUpdatedDigitCount() {
|
||||||
|
val digits = lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo digits
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whenDigitsEqualCodeLengthCompletionCallbackIsCalled() {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
lockScreenCodeView.onCodeCompleted = LockScreenCodeView.CodeCompletedListener { latch.countDown() }
|
||||||
|
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 4
|
||||||
|
latch.await(1, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whenCodeIsCompletedCannotAddMoreDigits() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 4
|
||||||
|
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 4
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun whenChangingCodeLengthCodeIsReset() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
|
||||||
|
lockScreenCodeView.codeLength = 10
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun changingCodeLengthToTheSameValueDoesNothing() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
|
||||||
|
lockScreenCodeView.codeLength = lockScreenCodeView.codeLength
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearResetsEnteredDigits() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
|
||||||
|
lockScreenCodeView.clearCode()
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteLastRemovesLastDigit() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 2
|
||||||
|
|
||||||
|
lockScreenCodeView.deleteLast()
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteLastReturnsUpdatedDigitCount() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
val digits = lockScreenCodeView.deleteLast()
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo digits
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteLastCannotRemoveDigitIfCodeIsEmpty() {
|
||||||
|
lockScreenCodeView.onCharInput('1')
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 1
|
||||||
|
|
||||||
|
lockScreenCodeView.deleteLast()
|
||||||
|
lockScreenCodeView.deleteLast()
|
||||||
|
|
||||||
|
lockScreenCodeView.enteredDigits shouldBeEqualTo 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,9 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
|
<!-- Used for UI tests to display the BiometricPrompt. It's normal that it appears as an error. -->
|
||||||
|
<activity android:exported="false" android:name=".features.pin.lockscreen.tests.LockScreenTestActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -195,7 +195,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
|
||||||
vectorPreferences.clearPreferences()
|
vectorPreferences.clearPreferences()
|
||||||
uiStateRepository.reset()
|
uiStateRepository.reset()
|
||||||
pinLocker.unlock()
|
pinLocker.unlock()
|
||||||
pinCodeStore.deleteEncodedPin()
|
pinCodeStore.deletePinCode()
|
||||||
vectorAnalytics.onSignOut()
|
vectorAnalytics.onSignOut()
|
||||||
vectorSessionStore.clear()
|
vectorSessionStore.clear()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
|
@ -30,7 +31,8 @@ import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class ReAuthViewModel @AssistedInject constructor(
|
class ReAuthViewModel @AssistedInject constructor(
|
||||||
@Assisted val initialState: ReAuthState,
|
@Assisted val initialState: ReAuthState,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
|
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -58,7 +60,7 @@ class ReAuthViewModel @AssistedInject constructor(
|
||||||
is ReAuthActions.ReAuthWithPass -> {
|
is ReAuthActions.ReAuthWithPass -> {
|
||||||
val safeForIntentCypher = ByteArrayOutputStream().also {
|
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||||
it.use {
|
it.use {
|
||||||
session.secureStorageService().securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it)
|
matrix.secureStorageService().securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it)
|
||||||
}
|
}
|
||||||
}.toByteArray().toBase64NoPadding()
|
}.toByteArray().toBase64NoPadding()
|
||||||
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher))
|
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher))
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.LiveEvent
|
import im.vector.app.core.utils.LiveEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -42,7 +43,8 @@ import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreSharedViewModel @Inject constructor(
|
class KeysBackupRestoreSharedViewModel @Inject constructor(
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
data class KeySource(
|
data class KeySource(
|
||||||
|
@ -186,7 +188,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
|
||||||
fun handleGotSecretFromSSSS(cipherData: String, alias: String) {
|
fun handleGotSecretFromSSSS(cipherData: String, alias: String) {
|
||||||
try {
|
try {
|
||||||
cipherData.fromBase64().inputStream().use { ins ->
|
cipherData.fromBase64().inputStream().use { ins ->
|
||||||
val res = session.secureStorageService().loadSecureSecret<Map<String, String>>(ins, alias)
|
val res = matrix.secureStorageService().loadSecureSecret<Map<String, String>>(ins, alias)
|
||||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME)
|
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME)
|
||||||
if (secret == null) {
|
if (secret == null) {
|
||||||
_navigateEvent.postValue(
|
_navigateEvent.postValue(
|
||||||
|
|
|
@ -36,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
|
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
|
||||||
|
@ -86,7 +87,8 @@ data class SharedSecureStorageViewState(
|
||||||
class SharedSecureStorageViewModel @AssistedInject constructor(
|
class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: SharedSecureStorageViewState,
|
@Assisted private val initialState: SharedSecureStorageViewState,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val matrix: Matrix,
|
||||||
) :
|
) :
|
||||||
VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) {
|
VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) {
|
||||||
|
|
||||||
|
@ -249,7 +251,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||||
val safeForIntentCypher = ByteArrayOutputStream().also {
|
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||||
it.use {
|
it.use {
|
||||||
session.secureStorageService().securelyStoreObject(decryptedSecretMap as Map<String, String>, initialState.resultKeyStoreAlias, it)
|
matrix.secureStorageService().securelyStoreObject(decryptedSecretMap as Map<String, String>, initialState.resultKeyStoreAlias, it)
|
||||||
}
|
}
|
||||||
}.toByteArray().toBase64NoPadding()
|
}.toByteArray().toBase64NoPadding()
|
||||||
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
||||||
|
@ -345,7 +347,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
|
||||||
val safeForIntentCypher = ByteArrayOutputStream().also {
|
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||||
it.use {
|
it.use {
|
||||||
session.secureStorageService().securelyStoreObject(decryptedSecretMap as Map<String, String>, initialState.resultKeyStoreAlias, it)
|
matrix.secureStorageService().securelyStoreObject(decryptedSecretMap as Map<String, String>, initialState.resultKeyStoreAlias, it)
|
||||||
}
|
}
|
||||||
}.toByteArray().toBase64NoPadding()
|
}.toByteArray().toBase64NoPadding()
|
||||||
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
|
||||||
|
|
|
@ -39,6 +39,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired
|
||||||
import im.vector.app.features.raw.wellknown.secureBackupMethod
|
import im.vector.app.features.raw.wellknown.secureBackupMethod
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
@ -70,6 +71,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
private val rawService: RawService,
|
private val rawService: RawService,
|
||||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||||
private val migrationTask: BackupToQuadSMigrationTask,
|
private val migrationTask: BackupToQuadSMigrationTask,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||||
|
|
||||||
private var doesKeyBackupExist: Boolean = false
|
private var doesKeyBackupExist: Boolean = false
|
||||||
|
@ -274,7 +276,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
|
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
|
||||||
}
|
}
|
||||||
is BootstrapActions.PasswordAuthDone -> {
|
is BootstrapActions.PasswordAuthDone -> {
|
||||||
val decryptedPass = session.secureStorageService()
|
val decryptedPass = matrix.secureStorageService()
|
||||||
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
uiaContinuation?.resume(
|
uiaContinuation?.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
|
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
@ -100,7 +101,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
private val rawService: RawService,
|
private val rawService: RawService,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider,
|
||||||
|
private val matrix: Matrix,
|
||||||
) :
|
) :
|
||||||
VectorViewModel<VerificationBottomSheetViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState),
|
VectorViewModel<VerificationBottomSheetViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState),
|
||||||
VerificationService.Listener {
|
VerificationService.Listener {
|
||||||
|
@ -402,7 +404,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
action.cypherData.fromBase64().inputStream().use { ins ->
|
action.cypherData.fromBase64().inputStream().use { ins ->
|
||||||
val res = session.secureStorageService().loadSecureSecret<Map<String, String>>(ins, action.alias)
|
val res = matrix.secureStorageService().loadSecureSecret<Map<String, String>>(ins, action.alias)
|
||||||
val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys(
|
val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys(
|
||||||
res?.get(MASTER_KEY_SSSS_NAME),
|
res?.get(MASTER_KEY_SSSS_NAME),
|
||||||
res?.get(USER_SIGNING_KEY_SSSS_NAME),
|
res?.get(USER_SIGNING_KEY_SSSS_NAME),
|
||||||
|
|
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
@ -63,7 +64,9 @@ class ShortcutsHandler @Inject constructor(
|
||||||
// No op
|
// No op
|
||||||
return Job()
|
return Job()
|
||||||
}
|
}
|
||||||
hasPinCode.set(pinCodeStore.getEncodedPin() != null)
|
coroutineScope.launch {
|
||||||
|
hasPinCode.set(pinCodeStore.hasEncodedPin())
|
||||||
|
}
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return Job()
|
val session = activeSessionHolder.getSafeActiveSession() ?: return Job()
|
||||||
return session.flow().liveRoomSummaries(
|
return session.flow().liveRoomSummaries(
|
||||||
roomSummaryQueryParams {
|
roomSummaryQueryParams {
|
||||||
|
|
|
@ -40,7 +40,7 @@ import javax.inject.Singleton
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class NotificationDrawerManager @Inject constructor(
|
class NotificationDrawerManager @Inject constructor(
|
||||||
private val context: Context,
|
context: Context,
|
||||||
private val notificationDisplayer: NotificationDisplayer,
|
private val notificationDisplayer: NotificationDisplayer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val activeSessionDataSource: ActiveSessionDataSource,
|
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||||
|
@ -72,7 +72,7 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createInitialNotificationState(): NotificationState {
|
private fun createInitialNotificationState(): NotificationState {
|
||||||
val queuedEvents = notificationEventPersistence.loadEvents(currentSession, factory = { rawEvents ->
|
val queuedEvents = notificationEventPersistence.loadEvents(factory = { rawEvents ->
|
||||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||||
})
|
})
|
||||||
val renderedEvents = queuedEvents.rawEvents().map { ProcessedEvent(ProcessedEvent.Type.KEEP, it) }.toMutableList()
|
val renderedEvents = queuedEvents.rawEvents().map { ProcessedEvent(ProcessedEvent.Type.KEEP, it) }.toMutableList()
|
||||||
|
@ -174,13 +174,13 @@ class NotificationDrawerManager @Inject constructor(
|
||||||
notificationState.clearAndAddRenderedEvents(eventsToRender)
|
notificationState.clearAndAddRenderedEvents(eventsToRender)
|
||||||
val session = currentSession ?: return
|
val session = currentSession ?: return
|
||||||
renderEvents(session, eventsToRender)
|
renderEvents(session, eventsToRender)
|
||||||
persistEvents(session)
|
persistEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistEvents(session: Session) {
|
private fun persistEvents() {
|
||||||
notificationState.queuedEvents { queuedEvents ->
|
notificationState.queuedEvents { queuedEvents ->
|
||||||
notificationEventPersistence.persistEvents(queuedEvents, session)
|
notificationEventPersistence.persistEvents(queuedEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package im.vector.app.features.notifications
|
package im.vector.app.features.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
@ -27,14 +27,17 @@ import javax.inject.Inject
|
||||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
||||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
||||||
|
|
||||||
class NotificationEventPersistence @Inject constructor(private val context: Context) {
|
class NotificationEventPersistence @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val matrix: Matrix,
|
||||||
|
) {
|
||||||
|
|
||||||
fun loadEvents(currentSession: Session?, factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||||
try {
|
try {
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.inputStream().use {
|
file.inputStream().use {
|
||||||
val events: ArrayList<NotifiableEvent>? = currentSession?.secureStorageService()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
val events: ArrayList<NotifiableEvent>? = matrix.secureStorageService().loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
||||||
if (events != null) {
|
if (events != null) {
|
||||||
return factory(events)
|
return factory(events)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +49,7 @@ class NotificationEventPersistence @Inject constructor(private val context: Cont
|
||||||
return factory(emptyList())
|
return factory(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistEvents(queuedEvents: NotificationEventQueue, currentSession: Session) {
|
fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||||
if (queuedEvents.isEmpty()) {
|
if (queuedEvents.isEmpty()) {
|
||||||
deleteCachedRoomNotifications(context)
|
deleteCachedRoomNotifications(context)
|
||||||
return
|
return
|
||||||
|
@ -55,7 +58,7 @@ class NotificationEventPersistence @Inject constructor(private val context: Cont
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||||
if (!file.exists()) file.createNewFile()
|
if (!file.exists()) file.createNewFile()
|
||||||
FileOutputStream(file).use {
|
FileOutputStream(file).use {
|
||||||
currentSession.secureStorageService().securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
matrix.secureStorageService().securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## Failed to save cached notification info")
|
Timber.e(e, "## Failed to save cached notification info")
|
||||||
|
|
|
@ -18,47 +18,38 @@ package im.vector.app.features.pin
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.beautycoder.pflockscreen.security.PFResult
|
import im.vector.app.features.pin.lockscreen.pincode.EncryptedPinCodeStorage
|
||||||
import com.beautycoder.pflockscreen.security.PFSecurityManager
|
|
||||||
import com.beautycoder.pflockscreen.security.callbacks.PFPinCodeHelperCallback
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
interface PinCodeStore {
|
interface PinCodeStore : EncryptedPinCodeStorage {
|
||||||
|
|
||||||
suspend fun storeEncodedPin(encodePin: String)
|
|
||||||
|
|
||||||
suspend fun deleteEncodedPin()
|
|
||||||
|
|
||||||
fun getEncodedPin(): String?
|
|
||||||
|
|
||||||
suspend fun hasEncodedPin(): Boolean
|
|
||||||
|
|
||||||
fun getRemainingPinCodeAttemptsNumber(): Int
|
|
||||||
|
|
||||||
fun getRemainingBiometricsAttemptsNumber(): Int
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will return the number of remaining attempts.
|
* Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time.
|
||||||
|
*/
|
||||||
|
fun getRemainingPinCodeAttemptsNumber(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should decrement the number of remaining PIN code attempts.
|
||||||
|
* @return The remaining attempts.
|
||||||
*/
|
*/
|
||||||
fun onWrongPin(): Int
|
fun onWrongPin(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will return the number of remaining attempts.
|
* Resets the counter of attempts for PIN code and biometric access.
|
||||||
*/
|
*/
|
||||||
fun onWrongBiometrics(): Int
|
fun resetCounter()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will reset the counters.
|
* Adds a listener to be notified when the PIN code us created or removed.
|
||||||
*/
|
*/
|
||||||
fun resetCounters()
|
|
||||||
|
|
||||||
fun addListener(listener: PinCodeStoreListener)
|
fun addListener(listener: PinCodeStoreListener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a listener to be notified when the PIN code us created or removed.
|
||||||
|
*/
|
||||||
fun removeListener(listener: PinCodeStoreListener)
|
fun removeListener(listener: PinCodeStoreListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,55 +58,41 @@ interface PinCodeStoreListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore {
|
class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences: SharedPreferences) : PinCodeStore, EncryptedPinCodeStorage {
|
||||||
private val listeners = mutableSetOf<PinCodeStoreListener>()
|
private val listeners = mutableSetOf<PinCodeStoreListener>()
|
||||||
|
|
||||||
override suspend fun storeEncodedPin(encodePin: String) {
|
override suspend fun getPinCode(): String? {
|
||||||
|
return sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun savePinCode(pinCode: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putString(ENCODED_PIN_CODE_KEY, encodePin)
|
putString(ENCODED_PIN_CODE_KEY, pinCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listeners.forEach { it.onPinSetUpChange(isConfigured = true) }
|
listeners.forEach { it.onPinSetUpChange(isConfigured = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteEncodedPin() {
|
override suspend fun deletePinCode() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Also reset the counters
|
// Also reset the counters
|
||||||
resetCounters()
|
resetCounter()
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
remove(ENCODED_PIN_CODE_KEY)
|
remove(ENCODED_PIN_CODE_KEY)
|
||||||
}
|
}
|
||||||
awaitPinCodeCallback<Boolean> {
|
|
||||||
PFSecurityManager.getInstance().pinCodeHelper.delete(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
listeners.forEach { it.onPinSetUpChange(isConfigured = false) }
|
listeners.forEach { it.onPinSetUpChange(isConfigured = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEncodedPin(): String? {
|
override suspend fun hasEncodedPin(): Boolean {
|
||||||
return sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null)
|
return withContext(Dispatchers.IO) { sharedPreferences.contains(ENCODED_PIN_CODE_KEY) }
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun hasEncodedPin(): Boolean = withContext(Dispatchers.IO) {
|
|
||||||
val hasEncodedPin = getEncodedPin()?.isNotBlank().orFalse()
|
|
||||||
if (!hasEncodedPin) {
|
|
||||||
return@withContext false
|
|
||||||
}
|
|
||||||
val result = awaitPinCodeCallback<Boolean> {
|
|
||||||
PFSecurityManager.getInstance().pinCodeHelper.isPinCodeEncryptionKeyExist(it)
|
|
||||||
}
|
|
||||||
result.error == null && result.result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRemainingPinCodeAttemptsNumber(): Int {
|
override fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
|
return sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRemainingBiometricsAttemptsNumber(): Int {
|
|
||||||
return sharedPreferences.getInt(REMAINING_BIOMETRICS_ATTEMPTS_KEY, MAX_BIOMETRIC_ATTEMPTS_NUMBER_BEFORE_FORCE_PIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWrongPin(): Int {
|
override fun onWrongPin(): Int {
|
||||||
val remaining = getRemainingPinCodeAttemptsNumber() - 1
|
val remaining = getRemainingPinCodeAttemptsNumber() - 1
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
|
@ -124,15 +101,7 @@ class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences:
|
||||||
return remaining
|
return remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWrongBiometrics(): Int {
|
override fun resetCounter() {
|
||||||
val remaining = getRemainingBiometricsAttemptsNumber() - 1
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putInt(REMAINING_BIOMETRICS_ATTEMPTS_KEY, remaining)
|
|
||||||
}
|
|
||||||
return remaining
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resetCounters() {
|
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
remove(REMAINING_PIN_CODE_ATTEMPTS_KEY)
|
remove(REMAINING_PIN_CODE_ATTEMPTS_KEY)
|
||||||
remove(REMAINING_BIOMETRICS_ATTEMPTS_KEY)
|
remove(REMAINING_BIOMETRICS_ATTEMPTS_KEY)
|
||||||
|
@ -147,16 +116,11 @@ class SharedPrefPinCodeStore @Inject constructor(private val sharedPreferences:
|
||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend inline fun <T> awaitPinCodeCallback(crossinline callback: (PFPinCodeHelperCallback<T>) -> Unit) = suspendCoroutine<PFResult<T>> { cont ->
|
|
||||||
callback(PFPinCodeHelperCallback<T> { result -> cont.resume(result) })
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
|
private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
|
||||||
private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY"
|
private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY"
|
||||||
private const val REMAINING_BIOMETRICS_ATTEMPTS_KEY = "REMAINING_BIOMETRICS_ATTEMPTS_KEY"
|
private const val REMAINING_BIOMETRICS_ATTEMPTS_KEY = "REMAINING_BIOMETRICS_ATTEMPTS_KEY"
|
||||||
|
|
||||||
private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3
|
private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3
|
||||||
private const val MAX_BIOMETRIC_ATTEMPTS_NUMBER_BEFORE_FORCE_PIN = 5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.beautycoder.pflockscreen.PFFLockScreenConfiguration
|
|
||||||
import com.beautycoder.pflockscreen.fragments.PFLockScreenFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
|
@ -35,9 +32,15 @@ import im.vector.app.core.utils.toast
|
||||||
import im.vector.app.databinding.FragmentPinBinding
|
import im.vector.app.databinding.FragmentPinBinding
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.MainActivityArgs
|
import im.vector.app.features.MainActivityArgs
|
||||||
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.AuthMethod
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.LockScreenFragment
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.LockScreenListener
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -47,7 +50,8 @@ data class PinArgs(
|
||||||
|
|
||||||
class PinFragment @Inject constructor(
|
class PinFragment @Inject constructor(
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
private val vectorPreferences: VectorPreferences
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
private val configuratorProvider: LockScreenConfiguratorProvider,
|
||||||
) : VectorBaseFragment<FragmentPinBinding>() {
|
) : VectorBaseFragment<FragmentPinBinding>() {
|
||||||
|
|
||||||
private val fragmentArgs: PinArgs by args()
|
private val fragmentArgs: PinArgs by args()
|
||||||
|
@ -66,77 +70,81 @@ class PinFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCreateFragment() {
|
private fun showCreateFragment() {
|
||||||
val createFragment = PFLockScreenFragment()
|
val createFragment = LockScreenFragment()
|
||||||
val builder = PFFLockScreenConfiguration.Builder(requireContext())
|
createFragment.lockScreenListener = object : LockScreenListener {
|
||||||
.setNewCodeValidation(true)
|
|
||||||
.setTitle(getString(R.string.create_pin_title))
|
|
||||||
.setNewCodeValidationTitle(getString(R.string.create_pin_confirm_title))
|
|
||||||
.setMode(PFFLockScreenConfiguration.MODE_CREATE)
|
|
||||||
|
|
||||||
createFragment.setConfiguration(builder.build())
|
|
||||||
createFragment.setCodeCreateListener(object : PFLockScreenFragment.OnPFLockScreenCodeCreateListener {
|
|
||||||
override fun onNewCodeValidationFailed() {
|
override fun onNewCodeValidationFailed() {
|
||||||
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPinCodeEnteredFirst(pinCode: String?): Boolean {
|
override fun onPinCodeCreated() {
|
||||||
return false
|
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
||||||
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCodeCreated(encodedCode: String) {
|
configuratorProvider.updateDefaultConfiguration {
|
||||||
lifecycleScope.launch {
|
copy(
|
||||||
pinCodeStore.storeEncodedPin(encodedCode)
|
mode = LockScreenMode.CREATE,
|
||||||
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
title = getString(R.string.create_pin_title),
|
||||||
vectorBaseActivity.finish()
|
needsNewCodeValidation = true,
|
||||||
}
|
newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title),
|
||||||
}
|
)
|
||||||
})
|
}
|
||||||
replaceFragment(R.id.pinFragmentContainer, createFragment)
|
replaceFragment(R.id.pinFragmentContainer, createFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showAuthFragment() {
|
private fun showAuthFragment() {
|
||||||
val encodedPin = pinCodeStore.getEncodedPin() ?: return
|
val authFragment = LockScreenFragment()
|
||||||
val authFragment = PFLockScreenFragment()
|
val canUseBiometrics = vectorPreferences.useBiometricsToUnlock()
|
||||||
val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0
|
authFragment.onLeftButtonClickedListener = View.OnClickListener { displayForgotPinWarningDialog() }
|
||||||
val builder = PFFLockScreenConfiguration.Builder(requireContext())
|
authFragment.lockScreenListener = object : LockScreenListener {
|
||||||
.setAutoShowBiometric(true)
|
override fun onAuthenticationFailure(authMethod: AuthMethod) {
|
||||||
.setUseBiometric(vectorPreferences.useBiometricsToUnlock() && canUseBiometrics)
|
when (authMethod) {
|
||||||
.setAutoShowBiometric(canUseBiometrics)
|
AuthMethod.PIN_CODE -> onWrongPin()
|
||||||
.setTitle(getString(R.string.auth_pin_title))
|
AuthMethod.BIOMETRICS -> Unit
|
||||||
.setLeftButton(getString(R.string.auth_pin_forgot))
|
|
||||||
.setClearCodeOnError(true)
|
|
||||||
.setMode(PFFLockScreenConfiguration.MODE_AUTH)
|
|
||||||
authFragment.setConfiguration(builder.build())
|
|
||||||
authFragment.setEncodedPinCode(encodedPin)
|
|
||||||
authFragment.setOnLeftButtonClickListener {
|
|
||||||
displayForgotPinWarningDialog()
|
|
||||||
}
|
|
||||||
authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
|
|
||||||
override fun onPinLoginFailed() {
|
|
||||||
onWrongPin()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthSuccessful() {
|
|
||||||
pinCodeStore.resetCounters()
|
|
||||||
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
|
||||||
vectorBaseActivity.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthLoginFailed() {
|
|
||||||
val remainingAttempts = pinCodeStore.onWrongBiometrics()
|
|
||||||
if (remainingAttempts <= 0) {
|
|
||||||
// Disable Biometrics
|
|
||||||
builder.setUseBiometric(false)
|
|
||||||
authFragment.setConfiguration(builder.build())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCodeInputSuccessful() {
|
override fun onAuthenticationSuccess(authMethod: AuthMethod) {
|
||||||
pinCodeStore.resetCounters()
|
pinCodeStore.resetCounter()
|
||||||
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
vectorBaseActivity.setResult(Activity.RESULT_OK)
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
override fun onAuthenticationError(authMethod: AuthMethod, throwable: Throwable) {
|
||||||
|
if (throwable is BiometricAuthError) {
|
||||||
|
// System disabled biometric auth, no need to do it ourselves
|
||||||
|
if (throwable.isAuthPermanentlyDisabledError) {
|
||||||
|
vectorPreferences.setUseBiometricToUnlock(false)
|
||||||
|
}
|
||||||
|
Toast.makeText(requireContext(), throwable.localizedMessage, Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Timber.e(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricKeyInvalidated() {
|
||||||
|
// Disable biometric auth in settings and remove system key
|
||||||
|
vectorPreferences.setUseBiometricToUnlock(false)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(R.string.auth_biometric_key_invalidated_message)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configuratorProvider.updateDefaultConfiguration {
|
||||||
|
copy(
|
||||||
|
mode = LockScreenMode.VERIFY,
|
||||||
|
title = getString(R.string.auth_pin_title),
|
||||||
|
isStrongBiometricsEnabled = isStrongBiometricsEnabled && canUseBiometrics,
|
||||||
|
isWeakBiometricsEnabled = isWeakBiometricsEnabled && canUseBiometrics,
|
||||||
|
isDeviceCredentialUnlockEnabled = isDeviceCredentialUnlockEnabled && canUseBiometrics,
|
||||||
|
autoStartBiometric = canUseBiometrics,
|
||||||
|
leftButtonTitle = getString(R.string.auth_pin_forgot),
|
||||||
|
clearCodeOnError = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
replaceFragment(R.id.pinFragmentContainer, authFragment)
|
replaceFragment(R.id.pinFragmentContainer, authFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +158,7 @@ class PinFragment @Inject constructor(
|
||||||
else -> {
|
else -> {
|
||||||
requireActivity().toast(R.string.too_many_pin_failures)
|
requireActivity().toast(R.string.too_many_pin_failures)
|
||||||
// Logout
|
// Logout
|
||||||
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true))
|
launchResetPinFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.biometrics
|
||||||
|
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for [BiometricPrompt.AuthenticationCallback] errors.
|
||||||
|
*/
|
||||||
|
class BiometricAuthError(val code: Int, message: String) : Throwable(message) {
|
||||||
|
/**
|
||||||
|
* This error disables Biometric authentication, either temporarily or permanently.
|
||||||
|
*/
|
||||||
|
val isAuthDisabledError: Boolean get() = code in LOCKOUT_ERROR_CODES
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This error permanently disables Biometric authentication.
|
||||||
|
*/
|
||||||
|
val isAuthPermanentlyDisabledError: Boolean get() = code == BiometricPrompt.ERROR_LOCKOUT_PERMANENT
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.biometrics
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.annotation.VisibleForTesting.PRIVATE
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
|
||||||
|
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
|
||||||
|
import im.vector.app.features.pin.lockscreen.utils.hasFlag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper to manage system authentication (biometric and other types) and the system key.
|
||||||
|
*/
|
||||||
|
class BiometricHelper @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val lockScreenKeyRepository: LockScreenKeyRepository,
|
||||||
|
private val configurationProvider: LockScreenConfiguratorProvider,
|
||||||
|
private val biometricManager: BiometricManager,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
) {
|
||||||
|
private var prompt: BiometricPrompt? = null
|
||||||
|
|
||||||
|
private val configuration: LockScreenConfiguration get() = configurationProvider.currentConfiguration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
|
||||||
|
*/
|
||||||
|
val canUseWeakBiometricAuth: Boolean get() =
|
||||||
|
configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
|
||||||
|
*/
|
||||||
|
val canUseStrongBiometricAuth: Boolean get() =
|
||||||
|
configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.).
|
||||||
|
*/
|
||||||
|
val canUseDeviceCredentialsAuth: Boolean get() =
|
||||||
|
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal val canUseAnySystemAuth: Boolean get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if any system authentication method and there is a valid associated key.
|
||||||
|
*/
|
||||||
|
val isSystemAuthEnabledAndValid: Boolean get() = canUseAnySystemAuth && isSystemKeyValid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true is the [KeyStore] contains a key associated to system authentication.
|
||||||
|
*/
|
||||||
|
val hasSystemKey: Boolean get() = lockScreenKeyRepository.hasSystemKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the system key is valid, that is, not invalidated by new enrollments.
|
||||||
|
*/
|
||||||
|
val isSystemKeyValid: Boolean get() = lockScreenKeyRepository.isSystemKeyValid()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables system authentication after displaying a [BiometricPrompt] in the passed [FragmentActivity].
|
||||||
|
* Note: Must be called from the Main thread.
|
||||||
|
* @return: A [Flow] with the [Boolean] success/failure result or a [BiometricAuthError].
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun enableAuthentication(activity: FragmentActivity): Flow<Boolean> {
|
||||||
|
return authenticateInternal(activity, checkSystemKeyExists = false, cryptoObject = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables system authentication cancelling the current [BiometricPrompt] if needed.
|
||||||
|
* Note: Must be called from the Main thread.
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun disableAuthentication() {
|
||||||
|
lockScreenKeyRepository.deleteSystemKey()
|
||||||
|
cancelPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a [BiometricPrompt] in the passed [FragmentActivity] and unlocking the system key if succeeds.
|
||||||
|
* Note: Must be called from the Main thread.
|
||||||
|
* @return: A [Flow] with the [Boolean] success/failure result or a [BiometricAuthError].
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun authenticate(activity: FragmentActivity): Flow<Boolean> {
|
||||||
|
return authenticateInternal(activity, checkSystemKeyExists = true, cryptoObject = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a [BiometricPrompt] in the passed [Fragment] and unlocking the system key if succeeds.
|
||||||
|
* Note: Must be called from the Main thread.
|
||||||
|
* @return: A [Flow] with the [Boolean] success/failure result or a [BiometricAuthError].
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun authenticate(fragment: Fragment): Flow<Boolean> {
|
||||||
|
val activity = fragment.activity ?: return flowOf(false)
|
||||||
|
return authenticate(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun authenticateInternal(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
checkSystemKeyExists: Boolean,
|
||||||
|
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||||
|
): Flow<Boolean> {
|
||||||
|
if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false)
|
||||||
|
|
||||||
|
if (prompt != null) {
|
||||||
|
cancelPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val channel = createAuthChannel()
|
||||||
|
prompt = authenticateWithPromptInternal(activity, cryptoObject, channel)
|
||||||
|
return flow {
|
||||||
|
// We need to listen to the channel until it's closed
|
||||||
|
while (!channel.isClosedForReceive) {
|
||||||
|
val result = channel.receiveCatching()
|
||||||
|
when (val exception = result.exceptionOrNull()) {
|
||||||
|
null -> result.getOrNull()?.let { emit(it) }
|
||||||
|
else -> {
|
||||||
|
// Exception found, stop collecting, throw it and remove the prompt reference
|
||||||
|
prompt = null
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generates the system key on successful authentication
|
||||||
|
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
|
||||||
|
lockScreenKeyRepository.ensureSystemKey()
|
||||||
|
}
|
||||||
|
// Channel is closed, remove prompt reference
|
||||||
|
prompt = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal fun authenticateWithPromptInternal(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||||
|
channel: Channel<Boolean>,
|
||||||
|
): BiometricPrompt {
|
||||||
|
val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher())
|
||||||
|
val authenticators = getAvailableAuthenticators()
|
||||||
|
val isUsingDeviceCredentialAuthenticator = authenticators.hasFlag(DEVICE_CREDENTIAL)
|
||||||
|
val cancelButtonTitle = configuration.biometricCancelButtonTitle ?: context.getString(R.string.lockscreen_cancel)
|
||||||
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(configuration.biometricTitle ?: context.getString(R.string.lockscreen_sign_in))
|
||||||
|
.apply {
|
||||||
|
configuration.biometricSubtitle?.let {
|
||||||
|
setSubtitle(it)
|
||||||
|
}
|
||||||
|
if (!isUsingDeviceCredentialAuthenticator) {
|
||||||
|
setNegativeButtonText(cancelButtonTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setAllowedAuthenticators(authenticators)
|
||||||
|
.build()
|
||||||
|
return BiometricPrompt(activity, executor, callback).also {
|
||||||
|
showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) {
|
||||||
|
// For some reason this seems to be needed unless we want to receive a fragment transaction exception
|
||||||
|
delay(1L)
|
||||||
|
if (cryptoObject != null) {
|
||||||
|
it.authenticate(promptInfo, cryptoObject)
|
||||||
|
} else {
|
||||||
|
it.authenticate(promptInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableAuthenticators(): Int {
|
||||||
|
var authenticators = 0
|
||||||
|
// Android 10 (Q) and below can only use a single authenticator at the same time
|
||||||
|
if (buildVersionSdkIntProvider.get() <= Build.VERSION_CODES.Q) {
|
||||||
|
authenticators = when {
|
||||||
|
canUseStrongBiometricAuth -> BIOMETRIC_STRONG
|
||||||
|
canUseWeakBiometricAuth -> BIOMETRIC_WEAK
|
||||||
|
canUseDeviceCredentialsAuth -> DEVICE_CREDENTIAL
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (canUseDeviceCredentialsAuth) {
|
||||||
|
authenticators += DEVICE_CREDENTIAL
|
||||||
|
}
|
||||||
|
if (canUseStrongBiometricAuth) {
|
||||||
|
authenticators += BIOMETRIC_STRONG
|
||||||
|
}
|
||||||
|
// We can't use BIOMETRIC_STRONG and BIOMETRIC_WEAK at the same time. We should prioritize BIOMETRIC_STRONG.
|
||||||
|
if (!authenticators.hasFlag(BIOMETRIC_STRONG) && canUseWeakBiometricAuth) {
|
||||||
|
authenticators += BIOMETRIC_WEAK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authenticators
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSuspendingAuthCallback(
|
||||||
|
channel: Channel<Boolean>,
|
||||||
|
coroutineContext: CoroutineContext,
|
||||||
|
): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
private val scope = CoroutineScope(coroutineContext)
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
scope.launch {
|
||||||
|
// Error is a terminal event, should close both the Channel and the CoroutineScope to free resources.
|
||||||
|
channel.close(BiometricAuthError(errorCode, errString.toString()))
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
scope.launch { channel.send(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
scope.launch {
|
||||||
|
channel.send(true)
|
||||||
|
// Success is a terminal event, should close both the Channel and the CoroutineScope to free resources.
|
||||||
|
channel.close()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method displays a fallback biometric prompt dialog for devices with issues with their system implementations.
|
||||||
|
* @param activity [FragmentActivity] to display this fallback fragment in.
|
||||||
|
* @param authenticationFLow [Flow] where the authentication events will be received.
|
||||||
|
* @param coroutineContext [CoroutineContext] to run async code. It's shared with the [BiometricPrompt] executor value.
|
||||||
|
* @param showPrompt Lambda containing the code to show the original [BiometricPrompt] above the fallback dialog.
|
||||||
|
* @see [DevicePromptCheck].
|
||||||
|
*/
|
||||||
|
private fun showFallbackFragmentIfNeeded(
|
||||||
|
activity: FragmentActivity,
|
||||||
|
authenticationFLow: Flow<Boolean>,
|
||||||
|
coroutineContext: CoroutineContext,
|
||||||
|
showPrompt: suspend () -> Unit
|
||||||
|
) {
|
||||||
|
val scope = CoroutineScope(coroutineContext)
|
||||||
|
if (DevicePromptCheck.isDeviceWithNoBiometricUI) {
|
||||||
|
val fallbackFragment = activity.supportFragmentManager.findFragmentByTag(FALLBACK_BIOMETRIC_FRAGMENT_TAG) as? FallbackBiometricDialogFragment
|
||||||
|
?: FallbackBiometricDialogFragment.instantiate(
|
||||||
|
title = configuration.biometricTitle,
|
||||||
|
description = configuration.biometricSubtitle,
|
||||||
|
cancelActionText = configuration.biometricCancelButtonTitle,
|
||||||
|
)
|
||||||
|
fallbackFragment.onDismiss = { cancelPrompt() }
|
||||||
|
fallbackFragment.authenticationFlow = authenticationFLow
|
||||||
|
|
||||||
|
activity.supportFragmentManager.beginTransaction()
|
||||||
|
.runOnCommit { scope.launch { showPrompt() } }
|
||||||
|
.apply { fallbackFragment.show(this, FALLBACK_BIOMETRIC_FRAGMENT_TAG) }
|
||||||
|
} else {
|
||||||
|
scope.launch { showPrompt() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal fun cancelPrompt() {
|
||||||
|
prompt?.cancelAuthentication()
|
||||||
|
prompt = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal fun createAuthChannel(): Channel<Boolean> = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FALLBACK_BIOMETRIC_FRAGMENT_TAG = "fragment.biometric_fallback"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.configuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration to be used by the lockscreen feature.
|
||||||
|
*/
|
||||||
|
data class LockScreenConfiguration(
|
||||||
|
/** Which mode should the UI display, [LockScreenMode.VERIFY] or [LockScreenMode.CREATE]. */
|
||||||
|
val mode: LockScreenMode,
|
||||||
|
/** Length in digits of the pin code. */
|
||||||
|
val pinCodeLength: Int,
|
||||||
|
/** Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported. */
|
||||||
|
val isStrongBiometricsEnabled: Boolean,
|
||||||
|
/** Authentication with weak methods (most face/iris unlock implementations) is supported. */
|
||||||
|
val isWeakBiometricsEnabled: Boolean,
|
||||||
|
/** Authentication with device credentials (system lockscreen pin code, password, pattern) is supported. */
|
||||||
|
val isDeviceCredentialUnlockEnabled: Boolean,
|
||||||
|
/** New pin code creation needs to be inputted twice for confirmation. */
|
||||||
|
val needsNewCodeValidation: Boolean,
|
||||||
|
/** Biometric authentication should be started automatically when the pin code screen is displayed. Defaults to true. */
|
||||||
|
val autoStartBiometric: Boolean = true,
|
||||||
|
/** Display a button in the bottom-left corner of the 'pin pad'. Defaults to true. */
|
||||||
|
val leftButtonVisible: Boolean = true,
|
||||||
|
/** Text of the button in the bottom-left corner of the 'pin pad'. Optional. */
|
||||||
|
val leftButtonTitle: String? = null,
|
||||||
|
/** Title of the pin code screen. Optional. */
|
||||||
|
val title: String? = null,
|
||||||
|
/** Subtitle of the pin code screen. Optional. */
|
||||||
|
val subtitle: String? = null,
|
||||||
|
/** Title of the 'confirm pin code' screen. Optional. */
|
||||||
|
val newCodeConfirmationTitle: String? = null,
|
||||||
|
/** Clear the inputted pin code on error. Defaults to true. */
|
||||||
|
val clearCodeOnError: Boolean = true,
|
||||||
|
/** Vibrate on authentication failed. Defaults to true. */
|
||||||
|
val vibrateOnError: Boolean = true,
|
||||||
|
/** Animated the pin code view on authentication failed. Defaults to true. */
|
||||||
|
val animateOnError: Boolean = true,
|
||||||
|
/** Title for the Biometric prompt dialog. Optional. */
|
||||||
|
val biometricTitle: String? = null,
|
||||||
|
/** Subtitle for the Biometric prompt dialog. Optional. */
|
||||||
|
val biometricSubtitle: String? = null,
|
||||||
|
/** Text for the cancel button of the Biometric prompt dialog. Optional. */
|
||||||
|
val biometricCancelButtonTitle: String? = null,
|
||||||
|
)
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.configuration
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to hold both the [defaultConfiguration] and an updated version in [currentConfiguration].
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class LockScreenConfiguratorProvider @Inject constructor(
|
||||||
|
/** Default [LockScreenConfiguration], any derived configuration created using [updateDefaultConfiguration] will use this as a base. */
|
||||||
|
val defaultConfiguration: LockScreenConfiguration,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val mutableConfigurationFlow = MutableStateFlow(defaultConfiguration)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Flow] that emits any changes in configuration.
|
||||||
|
*/
|
||||||
|
val configurationFlow: Flow<LockScreenConfiguration> = mutableConfigurationFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current configuration to be read and used.
|
||||||
|
*/
|
||||||
|
val currentConfiguration get() = mutableConfigurationFlow.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the changes in [block] to the [defaultConfiguration] to generate a new [currentConfiguration].
|
||||||
|
*/
|
||||||
|
fun updateDefaultConfiguration(block: LockScreenConfiguration.() -> LockScreenConfiguration) {
|
||||||
|
mutableConfigurationFlow.value = defaultConfiguration.block()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the [currentConfiguration] to the [defaultConfiguration].
|
||||||
|
*/
|
||||||
|
fun reset() {
|
||||||
|
mutableConfigurationFlow.value = defaultConfiguration
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.configuration
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.LockScreenViewModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mode used by [LockScreenViewModel] to configure the UI and interactions.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
enum class LockScreenMode : Parcelable {
|
||||||
|
CREATE,
|
||||||
|
VERIFY
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.biometrics.BiometricPrompt
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.KeyStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class to make working with KeyStore and keys easier.
|
||||||
|
*/
|
||||||
|
class KeyStoreCrypto @AssistedInject constructor(
|
||||||
|
@Assisted val alias: String,
|
||||||
|
@Assisted keyNeedsUserAuthentication: Boolean,
|
||||||
|
context: Context,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
private val keyStore: KeyStore,
|
||||||
|
// It's easier to test it this way
|
||||||
|
private val secretStoringUtils: SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider, keyNeedsUserAuthentication)
|
||||||
|
) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun provide(alias: String, keyNeedsUserAuthentication: Boolean): KeyStoreCrypto
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a [Key] for the [alias] exists and validates it.
|
||||||
|
* @throws KeyPermanentlyInvalidatedException if key is not valid.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
@Throws(KeyPermanentlyInvalidatedException::class)
|
||||||
|
fun ensureKey() = secretStoringUtils.ensureKey(alias).also {
|
||||||
|
// Check validity of Key by initializing an encryption Cipher
|
||||||
|
secretStoringUtils.getEncryptCipher(alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the [ByteArray] value passed using generated the crypto key.
|
||||||
|
*/
|
||||||
|
fun encrypt(value: ByteArray): ByteArray = secretStoringUtils.securelyStoreBytes(value, alias)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the [String] value passed using generated the crypto key.
|
||||||
|
*/
|
||||||
|
fun encrypt(value: String): ByteArray = encrypt(value.toByteArray())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the [ByteArray] value passed using generated the crypto key.
|
||||||
|
* @return A Base64 encoded String.
|
||||||
|
*/
|
||||||
|
fun encryptToString(value: ByteArray): String = Base64.encodeToString(encrypt(value), Base64.NO_WRAP)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the [String] value passed using generated the crypto key.
|
||||||
|
* @return A Base64 encoded String.
|
||||||
|
*/
|
||||||
|
fun encryptToString(value: String): String = Base64.encodeToString(encrypt(value), Base64.NO_WRAP)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the [ByteArray] value passed using the generated crypto key.
|
||||||
|
*/
|
||||||
|
fun decrypt(value: ByteArray): ByteArray = secretStoringUtils.loadSecureSecretBytes(value, alias)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the [String] value passed using the generated crypto key.
|
||||||
|
*/
|
||||||
|
fun decrypt(value: String): ByteArray = decrypt(Base64.decode(value, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the [ByteArray] value passed using the generated crypto key.
|
||||||
|
* @return The decrypted contents in as a String.
|
||||||
|
*/
|
||||||
|
fun decryptToString(value: ByteArray): String = String(decrypt(value))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the [String] value passed using the generated crypto key.
|
||||||
|
* @return The decrypted contents in as a String.
|
||||||
|
*/
|
||||||
|
fun decryptToString(value: String): String = String(decrypt(value))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the key associated with the [alias] is valid.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun hasValidKey(): Boolean {
|
||||||
|
val keyExists = hasKey()
|
||||||
|
return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) {
|
||||||
|
try {
|
||||||
|
ensureKey()
|
||||||
|
true
|
||||||
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keyExists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the key associated with the [alias] is stored in the [KeyStore].
|
||||||
|
*/
|
||||||
|
fun hasKey(): Boolean = keyStore.containsAlias(alias)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the key associated with the [alias] from the [KeyStore].
|
||||||
|
*/
|
||||||
|
fun deleteKey() = secretStoringUtils.safeDeleteKey(alias)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [BiometricPrompt.CryptoObject] to be used in authentication.
|
||||||
|
* @throws KeyPermanentlyInvalidatedException if key is invalidated.
|
||||||
|
*/
|
||||||
|
@Throws(KeyPermanentlyInvalidatedException::class)
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
fun getCryptoObject() = BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(alias))
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
object LockScreenCryptoConstants {
|
||||||
|
const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
const val LEGACY_PIN_CODE_KEY_ALIAS = "fp_pin_lock_screen_key_store"
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class in charge of managing both the PIN code key and the system authentication keys.
|
||||||
|
*/
|
||||||
|
class LockScreenKeyRepository(
|
||||||
|
baseName: String,
|
||||||
|
private val pinCodeMigrator: PinCodeMigrator,
|
||||||
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
private val keyStoreCryptoFactory: KeyStoreCrypto.Factory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val pinCodeKeyAlias = "$baseName.pin_code"
|
||||||
|
private val systemKeyAlias = "$baseName.system"
|
||||||
|
|
||||||
|
private val pinCodeCrypto: KeyStoreCrypto by lazy {
|
||||||
|
keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false)
|
||||||
|
}
|
||||||
|
private val systemKeyCrypto: KeyStoreCrypto by lazy {
|
||||||
|
keyStoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the [pinCode], creating the associated key if needed.
|
||||||
|
*/
|
||||||
|
fun encryptPinCode(pinCode: String): String = pinCodeCrypto.encryptToString(pinCode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the [encodedPinCode] into a plain [String] or null.
|
||||||
|
*/
|
||||||
|
fun decryptPinCode(encodedPinCode: String): String = pinCodeCrypto.decryptToString(encodedPinCode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the key associated to the system authentication (biometrics). It will be created if it didn't exist before.
|
||||||
|
* Note: this key will be invalidated by new biometric enrollments.
|
||||||
|
* @throws KeyPermanentlyInvalidatedException if key is invalidated.
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun ensureSystemKey() = systemKeyCrypto.ensureKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the PIN code key already exists.
|
||||||
|
*/
|
||||||
|
fun hasPinCodeKey() = pinCodeCrypto.hasKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the system authentication key already exists.
|
||||||
|
*/
|
||||||
|
fun hasSystemKey() = systemKeyCrypto.hasKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the PIN code key from the KeyStore.
|
||||||
|
*/
|
||||||
|
fun deletePinCodeKey() = pinCodeCrypto.deleteKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the system authentication key from the KeyStore.
|
||||||
|
*/
|
||||||
|
fun deleteSystemKey() = systemKeyCrypto.deleteKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current system authentication key exists and is valid.
|
||||||
|
*/
|
||||||
|
fun isSystemKeyValid() = systemKeyCrypto.hasValidKey()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the PIN code key and encrypted value to use a more secure cipher, also creates a new system key if needed.
|
||||||
|
*/
|
||||||
|
suspend fun migrateKeysIfNeeded() {
|
||||||
|
if (pinCodeMigrator.isMigrationNeeded()) {
|
||||||
|
pinCodeMigrator.migrate(pinCodeKeyAlias)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
|
||||||
|
try {
|
||||||
|
ensureSystemKey()
|
||||||
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
|
Timber.e("Could not automatically create biometric key.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.crypto
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to migrate from the old PIN code key ciphers to a more secure ones.
|
||||||
|
*/
|
||||||
|
class PinCodeMigrator @Inject constructor(
|
||||||
|
private val pinCodeStore: PinCodeStore,
|
||||||
|
private val keyStore: KeyStore,
|
||||||
|
private val secretStoringUtils: SecretStoringUtils,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val legacyKey: Key get() = keyStore.getKey(LEGACY_PIN_CODE_KEY_ALIAS, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates from the old ciphers and [LEGACY_PIN_CODE_KEY_ALIAS] to the [newAlias].
|
||||||
|
*/
|
||||||
|
suspend fun migrate(newAlias: String) {
|
||||||
|
if (!keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
|
||||||
|
|
||||||
|
val pinCode = getDecryptedPinCode() ?: return
|
||||||
|
val encryptedBytes = secretStoringUtils.securelyStoreBytes(pinCode.toByteArray(), newAlias)
|
||||||
|
val encryptedPinCode = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
|
||||||
|
pinCodeStore.savePinCode(encryptedPinCode)
|
||||||
|
keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMigrationNeeded(): Boolean = keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal suspend fun getDecryptedPinCode(): String? {
|
||||||
|
val encryptedPinCode = pinCodeStore.getPinCode() ?: return null
|
||||||
|
val cipher = getDecodeCipher()
|
||||||
|
val bytes = cipher.doFinal(Base64.decode(encryptedPinCode, Base64.NO_WRAP))
|
||||||
|
return String(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDecodeCipher(): Cipher {
|
||||||
|
return when (buildVersionSdkIntProvider.get()) {
|
||||||
|
Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL()
|
||||||
|
else -> getCipherM()
|
||||||
|
}.also { it.init(Cipher.DECRYPT_MODE, legacyKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCipherL(): Cipher {
|
||||||
|
// We cannot mock this in tests as it's tied to the actual cryptographic implementation of the OS version
|
||||||
|
val provider = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround"
|
||||||
|
val transformation = "RSA/ECB/PKCS1Padding"
|
||||||
|
return Cipher.getInstance(transformation, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCipherM(): Cipher {
|
||||||
|
val transformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
|
||||||
|
return Cipher.getInstance(transformation)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.IntoMap
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.MavericksViewModelKey
|
||||||
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.PinCodeMigrator
|
||||||
|
import im.vector.app.features.pin.lockscreen.pincode.EncryptedPinCodeStorage
|
||||||
|
import im.vector.app.features.pin.lockscreen.ui.LockScreenViewModel
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
|
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object LockScreenModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideBuildVersionSdkIntProvider(): BuildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideSecretStoringUtils(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
keyStore: KeyStore,
|
||||||
|
buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
) = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideLockScreenConfig() = LockScreenConfiguration(
|
||||||
|
mode = LockScreenMode.VERIFY,
|
||||||
|
pinCodeLength = 4,
|
||||||
|
isWeakBiometricsEnabled = false,
|
||||||
|
isDeviceCredentialUnlockEnabled = false,
|
||||||
|
isStrongBiometricsEnabled = true,
|
||||||
|
needsNewCodeValidation = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideKeyRepository(
|
||||||
|
pinCodeMigrator: PinCodeMigrator,
|
||||||
|
vectorPreferences: VectorPreferences,
|
||||||
|
keyStoreCryptoFactory: KeyStoreCrypto.Factory,
|
||||||
|
) = LockScreenKeyRepository(
|
||||||
|
baseName = "vector",
|
||||||
|
pinCodeMigrator,
|
||||||
|
vectorPreferences,
|
||||||
|
keyStoreCryptoFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideBiometricManager(@ApplicationContext context: Context) = BiometricManager.from(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface LockScreenBindsModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(LockScreenViewModel::class)
|
||||||
|
fun bindLockScreenViewModel(factory: LockScreenViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindSharedPreferencesStorage(pinCodeStore: PinCodeStore): EncryptedPinCodeStorage
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.pincode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be implemented by any class that provides access to the encrypted PIN code.
|
||||||
|
* All methods are suspending in case there are async IO operations involved.
|
||||||
|
*/
|
||||||
|
interface EncryptedPinCodeStorage {
|
||||||
|
/**
|
||||||
|
* Returns the encrypted PIN code.
|
||||||
|
*/
|
||||||
|
suspend fun getPinCode(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the encrypted PIN code to some persistable storage.
|
||||||
|
*/
|
||||||
|
suspend fun savePinCode(pinCode: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the PIN code from some persistable storage.
|
||||||
|
*/
|
||||||
|
suspend fun deletePinCode()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the encrypted PIN code is stored or not.
|
||||||
|
*/
|
||||||
|
suspend fun hasEncodedPin(): Boolean
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.pincode
|
||||||
|
|
||||||
|
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class to manage the PIN code creation, verification, removal and migration.
|
||||||
|
*/
|
||||||
|
class PinCodeHelper @Inject constructor(
|
||||||
|
private val lockScreenKeyRepository: LockScreenKeyRepository,
|
||||||
|
private val encryptedStorage: EncryptedPinCodeStorage,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if PIN code is available (both the key exists and the encrypted value is stored).
|
||||||
|
*/
|
||||||
|
suspend fun isPinCodeAvailable() = lockScreenKeyRepository.hasPinCodeKey() && encryptedStorage.getPinCode() != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a PIN code key if needed and stores the PIN code encrypted with it.
|
||||||
|
*/
|
||||||
|
suspend fun createPinCode(pinCode: String) {
|
||||||
|
val encryptedValue = lockScreenKeyRepository.encryptPinCode(pinCode)
|
||||||
|
encryptedStorage.savePinCode(encryptedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the passed [pinCode] against the encrypted one.
|
||||||
|
*/
|
||||||
|
suspend fun verifyPinCode(pinCode: String): Boolean {
|
||||||
|
val encryptedPinCode = encryptedStorage.getPinCode() ?: return false
|
||||||
|
return lockScreenKeyRepository.decryptPinCode(encryptedPinCode) == pinCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the store PIN code as well as the associated key.
|
||||||
|
*/
|
||||||
|
suspend fun deletePinCode() {
|
||||||
|
encryptedStorage.deletePinCode()
|
||||||
|
lockScreenKeyRepository.deletePinCodeKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the PIN code key and encrypted value to use a more secure cipher.
|
||||||
|
*/
|
||||||
|
suspend fun migratePinCodeIfNeeded() {
|
||||||
|
lockScreenKeyRepository.migrateKeysIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class LockScreenAction : VectorViewModelAction {
|
||||||
|
data class PinCodeEntered(val value: String) : LockScreenAction()
|
||||||
|
data class ShowBiometricPrompt(val callingActivity: FragmentActivity) : LockScreenAction()
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.hardware.vibrate
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.databinding.FragmentLockScreenBinding
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.views.LockScreenCodeView
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
|
||||||
|
|
||||||
|
var lockScreenListener: LockScreenListener? = null
|
||||||
|
var onLeftButtonClickedListener: View.OnClickListener? = null
|
||||||
|
|
||||||
|
private val viewModel: LockScreenViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLockScreenBinding =
|
||||||
|
FragmentLockScreenBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setupBindings(views)
|
||||||
|
|
||||||
|
viewModel.observeViewEvents {
|
||||||
|
handleEvent(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
withState(viewModel) { state ->
|
||||||
|
if (state.lockScreenConfiguration.mode == LockScreenMode.CREATE) return@withState
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||||
|
if (state.isBiometricKeyInvalidated) {
|
||||||
|
lockScreenListener?.onBiometricKeyInvalidated()
|
||||||
|
} else if (state.showBiometricPromptAutomatically) {
|
||||||
|
showBiometricPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
viewModel.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
when (state.pinCodeState) {
|
||||||
|
is PinCodeState.FirstCodeEntered -> {
|
||||||
|
setupTitleView(views.titleTextView, true, state.lockScreenConfiguration)
|
||||||
|
lockScreenListener?.onFirstCodeEntered()
|
||||||
|
}
|
||||||
|
is PinCodeState.Idle -> {
|
||||||
|
setupTitleView(views.titleTextView, false, state.lockScreenConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderDeleteOrFingerprintButtons(views, views.codeView.enteredDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAuthFailure(method: AuthMethod) {
|
||||||
|
lockScreenListener?.onAuthenticationFailure(method)
|
||||||
|
|
||||||
|
val configuration = withState(viewModel) { it.lockScreenConfiguration }
|
||||||
|
if (configuration.vibrateOnError) {
|
||||||
|
vibrate(requireContext(), 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.animateOnError) {
|
||||||
|
context?.let {
|
||||||
|
val animation = AnimationUtils.loadAnimation(it, R.anim.lockscreen_shake_animation)
|
||||||
|
views.codeView.startAnimation(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAuthError(authMethod: AuthMethod, throwable: Throwable) {
|
||||||
|
lockScreenListener?.onAuthenticationError(authMethod, throwable)
|
||||||
|
withState(viewModel) { state ->
|
||||||
|
if (state.lockScreenConfiguration.clearCodeOnError) {
|
||||||
|
views.codeView.clearCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEvent(viewEvent: LockScreenViewEvent) {
|
||||||
|
when (viewEvent) {
|
||||||
|
is LockScreenViewEvent.CodeCreationComplete -> lockScreenListener?.onPinCodeCreated()
|
||||||
|
is LockScreenViewEvent.ClearPinCode -> {
|
||||||
|
if (viewEvent.confirmationFailed) {
|
||||||
|
lockScreenListener?.onNewCodeValidationFailed()
|
||||||
|
}
|
||||||
|
views.codeView.clearCode()
|
||||||
|
}
|
||||||
|
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
|
||||||
|
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
|
||||||
|
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBindings(binding: FragmentLockScreenBinding) = with(binding) {
|
||||||
|
val configuration = withState(viewModel) { it.lockScreenConfiguration }
|
||||||
|
val lockScreenMode = configuration.mode
|
||||||
|
|
||||||
|
configuration.title?.let { titleTextView.text = it }
|
||||||
|
configuration.subtitle?.let {
|
||||||
|
subtitleTextView.text = it
|
||||||
|
subtitleTextView.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTitleView(titleTextView, false, configuration)
|
||||||
|
setupCodeView(codeView, configuration)
|
||||||
|
setupCodeButton('0', button0, this)
|
||||||
|
setupCodeButton('1', button1, this)
|
||||||
|
setupCodeButton('2', button2, this)
|
||||||
|
setupCodeButton('3', button3, this)
|
||||||
|
setupCodeButton('4', button4, this)
|
||||||
|
setupCodeButton('5', button5, this)
|
||||||
|
setupCodeButton('6', button6, this)
|
||||||
|
setupCodeButton('7', button7, this)
|
||||||
|
setupCodeButton('8', button8, this)
|
||||||
|
setupCodeButton('9', button9, this)
|
||||||
|
setupDeleteButton(buttonDelete, this)
|
||||||
|
setupFingerprintButton(buttonFingerPrint)
|
||||||
|
setupLeftButton(buttonLeft, lockScreenMode, configuration)
|
||||||
|
renderDeleteOrFingerprintButtons(this, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupTitleView(titleView: TextView, isConfirmation: Boolean, configuration: LockScreenConfiguration) = with(titleView) {
|
||||||
|
text = if (isConfirmation) {
|
||||||
|
configuration.newCodeConfirmationTitle ?: getString(R.string.lockscreen_confirm_pin)
|
||||||
|
} else {
|
||||||
|
configuration.title ?: getString(R.string.lockscreen_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCodeView(lockScreenCodeView: LockScreenCodeView, configuration: LockScreenConfiguration) = with(lockScreenCodeView) {
|
||||||
|
codeLength = configuration.pinCodeLength
|
||||||
|
onCodeCompleted = LockScreenCodeView.CodeCompletedListener { code ->
|
||||||
|
viewModel.handle(LockScreenAction.PinCodeEntered(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCodeButton(value: Char, view: View, binding: FragmentLockScreenBinding) {
|
||||||
|
view.setOnClickListener {
|
||||||
|
val size = binding.codeView.onCharInput(value)
|
||||||
|
renderDeleteOrFingerprintButtons(binding, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDeleteButton(view: View, binding: FragmentLockScreenBinding) {
|
||||||
|
view.setOnClickListener {
|
||||||
|
val size = binding.codeView.deleteLast()
|
||||||
|
renderDeleteOrFingerprintButtons(binding, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFingerprintButton(view: View) {
|
||||||
|
view.setOnClickListener {
|
||||||
|
showBiometricPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupLeftButton(view: TextView, lockScreenMode: LockScreenMode, configuration: LockScreenConfiguration) = with(view) {
|
||||||
|
isVisible = lockScreenMode == LockScreenMode.VERIFY && configuration.leftButtonVisible
|
||||||
|
configuration.leftButtonTitle?.let { text = it }
|
||||||
|
setOnClickListener(onLeftButtonClickedListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderDeleteOrFingerprintButtons(binding: FragmentLockScreenBinding, digits: Int) = withState(viewModel) { state ->
|
||||||
|
val showFingerprintButton = state.canUseBiometricAuth && !state.isBiometricKeyInvalidated && digits == 0
|
||||||
|
binding.buttonFingerPrint.isVisible = showFingerprintButton
|
||||||
|
binding.buttonDelete.isVisible = !showFingerprintButton && digits > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showBiometricPrompt() {
|
||||||
|
viewModel.handle(LockScreenAction.ShowBiometricPrompt(requireActivity()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener class to be notified of any event that could happen in the lock screen UI.
|
||||||
|
*/
|
||||||
|
interface LockScreenListener {
|
||||||
|
/**
|
||||||
|
* In PIN creation mode, called when the first PIN code has been entered.
|
||||||
|
*/
|
||||||
|
fun onFirstCodeEntered() = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In PIN creation mode, called when the confirmation PIN code doesn't match the first one.
|
||||||
|
*/
|
||||||
|
fun onNewCodeValidationFailed() = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In PIN creation mode, called when the PIN code was successfully set up.
|
||||||
|
*/
|
||||||
|
fun onPinCodeCreated() = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In verification mode, called when the authentication succeeded.
|
||||||
|
* @param authMethod Authentication method used ([AuthMethod.PIN_CODE] or [AuthMethod.BIOMETRICS]).
|
||||||
|
*/
|
||||||
|
fun onAuthenticationSuccess(authMethod: AuthMethod) = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In verification mode, called when the authentication failed. At this point the user can usually still retry the authentication.
|
||||||
|
* @param authMethod Authentication method used ([AuthMethod.PIN_CODE] or [AuthMethod.BIOMETRICS]).
|
||||||
|
*/
|
||||||
|
fun onAuthenticationFailure(authMethod: AuthMethod) = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In verification mode, called when the authentication had a fatal error and can't continue. This is not an authentication failure.
|
||||||
|
* @param authMethod Authentication method used ([AuthMethod.PIN_CODE] or [AuthMethod.BIOMETRICS]).
|
||||||
|
* @param throwable The error thrown when the authentication flow was interrupted.
|
||||||
|
*/
|
||||||
|
fun onAuthenticationError(authMethod: AuthMethod, throwable: Throwable) = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In verification mode, called when the system authentication key (used for biometrics) has been invalidated and cannot be used anymore.
|
||||||
|
*/
|
||||||
|
fun onBiometricKeyInvalidated() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum containing the available authentication methods.
|
||||||
|
*/
|
||||||
|
enum class AuthMethod {
|
||||||
|
PIN_CODE,
|
||||||
|
BIOMETRICS
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class LockScreenViewEvent : VectorViewEvents {
|
||||||
|
data class ClearPinCode(val confirmationFailed: Boolean) : LockScreenViewEvent()
|
||||||
|
object CodeCreationComplete : LockScreenViewEvent()
|
||||||
|
data class AuthSuccessful(val method: AuthMethod) : LockScreenViewEvent()
|
||||||
|
data class AuthFailure(val method: AuthMethod) : LockScreenViewEvent()
|
||||||
|
data class AuthError(val method: AuthMethod, val throwable: Throwable) : LockScreenViewEvent()
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
|
||||||
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
|
||||||
|
|
||||||
|
class LockScreenViewModel @AssistedInject constructor(
|
||||||
|
@Assisted val initialState: LockScreenViewState,
|
||||||
|
private val pinCodeHelper: PinCodeHelper,
|
||||||
|
private val biometricHelper: BiometricHelper,
|
||||||
|
private val configuratorProvider: LockScreenConfiguratorProvider,
|
||||||
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
|
) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : MavericksAssistedViewModelFactory<LockScreenViewModel, LockScreenViewState> {
|
||||||
|
override fun create(initialState: LockScreenViewState): LockScreenViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<LockScreenViewModel, LockScreenViewState> by hiltMavericksViewModelFactory() {
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): LockScreenViewState {
|
||||||
|
return LockScreenViewState(
|
||||||
|
lockScreenConfiguration = DUMMY_CONFIGURATION,
|
||||||
|
canUseBiometricAuth = false,
|
||||||
|
showBiometricPromptAutomatically = false,
|
||||||
|
pinCodeState = PinCodeState.Idle,
|
||||||
|
isBiometricKeyInvalidated = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DUMMY_CONFIGURATION = LockScreenConfiguration(
|
||||||
|
mode = LockScreenMode.VERIFY,
|
||||||
|
pinCodeLength = 4,
|
||||||
|
isStrongBiometricsEnabled = false,
|
||||||
|
isDeviceCredentialUnlockEnabled = false,
|
||||||
|
isWeakBiometricsEnabled = false,
|
||||||
|
needsNewCodeValidation = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var firstEnteredCode: String? = null
|
||||||
|
|
||||||
|
// BiometricPrompt will automatically disable system auth after too many failed auth attempts
|
||||||
|
private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
// We need this to run synchronously before we start reading the configurations
|
||||||
|
runBlocking { pinCodeHelper.migratePinCodeIfNeeded() }
|
||||||
|
|
||||||
|
configuratorProvider.configurationFlow
|
||||||
|
.onEach { updateConfiguration(it) }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: LockScreenAction) {
|
||||||
|
when (action) {
|
||||||
|
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
|
||||||
|
is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPinCodeEntered(code: String) = flow {
|
||||||
|
val state = awaitState()
|
||||||
|
when (state.lockScreenConfiguration.mode) {
|
||||||
|
LockScreenMode.CREATE -> {
|
||||||
|
if (firstEnteredCode == null && state.lockScreenConfiguration.needsNewCodeValidation) {
|
||||||
|
firstEnteredCode = code
|
||||||
|
_viewEvents.post(LockScreenViewEvent.ClearPinCode(false))
|
||||||
|
emit(PinCodeState.FirstCodeEntered)
|
||||||
|
} else {
|
||||||
|
if (!state.lockScreenConfiguration.needsNewCodeValidation || code == firstEnteredCode) {
|
||||||
|
pinCodeHelper.createPinCode(code)
|
||||||
|
_viewEvents.post(LockScreenViewEvent.CodeCreationComplete)
|
||||||
|
emit(null)
|
||||||
|
} else {
|
||||||
|
firstEnteredCode = null
|
||||||
|
_viewEvents.post(LockScreenViewEvent.ClearPinCode(true))
|
||||||
|
emit(PinCodeState.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockScreenMode.VERIFY -> {
|
||||||
|
if (pinCodeHelper.verifyPinCode(code)) {
|
||||||
|
_viewEvents.post(LockScreenViewEvent.AuthSuccessful(AuthMethod.PIN_CODE))
|
||||||
|
emit(null)
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(LockScreenViewEvent.AuthFailure(AuthMethod.PIN_CODE))
|
||||||
|
emit(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.catch { error ->
|
||||||
|
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.PIN_CODE, error))
|
||||||
|
}.onEach { newPinState ->
|
||||||
|
newPinState?.let { setState { copy(pinCodeState = it) } }
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private fun showBiometricPrompt(activity: FragmentActivity) = flow {
|
||||||
|
emitAll(biometricHelper.authenticate(activity))
|
||||||
|
}.catch { error ->
|
||||||
|
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException) {
|
||||||
|
removeBiometricAuthentication()
|
||||||
|
} else if (error is BiometricAuthError && error.isAuthDisabledError) {
|
||||||
|
isSystemAuthTemporarilyDisabledByBiometricPrompt = true
|
||||||
|
updateStateWithBiometricInfo()
|
||||||
|
}
|
||||||
|
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, error))
|
||||||
|
}.onEach { success ->
|
||||||
|
_viewEvents.post(
|
||||||
|
if (success) LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS)
|
||||||
|
else LockScreenViewEvent.AuthFailure(AuthMethod.BIOMETRICS)
|
||||||
|
)
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
configuratorProvider.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeBiometricAuthentication() {
|
||||||
|
biometricHelper.disableAuthentication()
|
||||||
|
updateStateWithBiometricInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStateWithBiometricInfo() {
|
||||||
|
val configuration = withState(this) { it.lockScreenConfiguration }
|
||||||
|
val canUseBiometricAuth = configuration.mode == LockScreenMode.VERIFY &&
|
||||||
|
!isSystemAuthTemporarilyDisabledByBiometricPrompt &&
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid
|
||||||
|
val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid
|
||||||
|
val showBiometricPromptAutomatically = canUseBiometricAuth &&
|
||||||
|
configuration.autoStartBiometric
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
canUseBiometricAuth = canUseBiometricAuth,
|
||||||
|
showBiometricPromptAutomatically = showBiometricPromptAutomatically,
|
||||||
|
isBiometricKeyInvalidated = isBiometricKeyInvalidated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateConfiguration(configuration: LockScreenConfiguration) {
|
||||||
|
setState { copy(lockScreenConfiguration = configuration) }
|
||||||
|
updateStateWithBiometricInfo()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
|
||||||
|
|
||||||
|
data class LockScreenViewState(
|
||||||
|
val lockScreenConfiguration: LockScreenConfiguration,
|
||||||
|
val canUseBiometricAuth: Boolean,
|
||||||
|
val showBiometricPromptAutomatically: Boolean,
|
||||||
|
val pinCodeState: PinCodeState,
|
||||||
|
val isBiometricKeyInvalidated: Boolean,
|
||||||
|
) : MavericksState
|
||||||
|
|
||||||
|
sealed class PinCodeState {
|
||||||
|
object Idle : PinCodeState()
|
||||||
|
object FirstCodeEntered : PinCodeState()
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.ui.fallbackprompt
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.airbnb.mvrx.Mavericks
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.databinding.FragmentBiometricDialogContainerBinding
|
||||||
|
import im.vector.app.databinding.ViewBiometricDialogContentBinding
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fragment to be displayed on devices that have issues with [BiometricPrompt].
|
||||||
|
*/
|
||||||
|
class FallbackBiometricDialogFragment : DialogFragment(R.layout.fragment_biometric_dialog_container) {
|
||||||
|
|
||||||
|
var onDismiss: (() -> Unit)? = null
|
||||||
|
|
||||||
|
var authenticationFlow: Flow<Boolean>? = null
|
||||||
|
|
||||||
|
private var binding: ViewBiometricDialogContentBinding? = null
|
||||||
|
|
||||||
|
private val parsedArgs by args<Args>()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
retainInstance = true
|
||||||
|
|
||||||
|
setStyle(STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
FragmentBiometricDialogContainerBinding.bind(view).apply {
|
||||||
|
parsedArgs.cancelActionText?.let { cancelButton.text = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val content = view.findViewById<ViewGroup>(R.id.dialogContent).getChildAt(0)
|
||||||
|
binding = ViewBiometricDialogContentBinding.bind(content).apply {
|
||||||
|
parsedArgs.description?.let { fingerprintDescription.text = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
requireDialog().setTitle(parsedArgs.title ?: getString(R.string.lockscreen_sign_in))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
val authFlow = authenticationFlow ?: return
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
authFlow.catch {
|
||||||
|
dismiss()
|
||||||
|
}.collect { success ->
|
||||||
|
if (success) {
|
||||||
|
renderSuccess()
|
||||||
|
} else {
|
||||||
|
renderFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderSuccess() {
|
||||||
|
val contentBinding = binding ?: return
|
||||||
|
contentBinding.fingerprintIcon.setImageResource(R.drawable.ic_fingerprint_success_lockscreen)
|
||||||
|
contentBinding.fingerprintStatus.apply {
|
||||||
|
setTextColor(ResourcesCompat.getColor(resources, R.color.lockscreen_success_color, null))
|
||||||
|
setText(R.string.lockscreen_fingerprint_success)
|
||||||
|
}
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
delay(200L)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFailure() {
|
||||||
|
val contentBinding = binding ?: return
|
||||||
|
contentBinding.fingerprintIcon.setImageResource(R.drawable.ic_fingerprint_error_lockscreen)
|
||||||
|
contentBinding.fingerprintStatus.apply {
|
||||||
|
setTextColor(ResourcesCompat.getColor(resources, R.color.lockscreen_warning_color, null))
|
||||||
|
setText(R.string.lockscreen_fingerprint_not_recognized)
|
||||||
|
}
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
delay(1500L)
|
||||||
|
resetState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetState() {
|
||||||
|
val contentBinding = binding ?: return
|
||||||
|
contentBinding.fingerprintIcon.setImageResource(R.drawable.lockscreen_fingerprint_40)
|
||||||
|
contentBinding.fingerprintStatus.apply {
|
||||||
|
setTextColor(ResourcesCompat.getColor(resources, R.color.lockscreen_hint_color, null))
|
||||||
|
setText(R.string.lockscreen_fingerprint_hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
|
||||||
|
onDismiss?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val title: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val cancelActionText: String? = null,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun instantiate(
|
||||||
|
title: String? = null,
|
||||||
|
description: String? = null,
|
||||||
|
cancelActionText: String? = null,
|
||||||
|
): FallbackBiometricDialogFragment {
|
||||||
|
return FallbackBiometricDialogFragment().also {
|
||||||
|
val args = Args(title, description, cancelActionText)
|
||||||
|
it.arguments = bundleOf(Mavericks.KEY_ARG to args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.utils
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to detect devices with [BiometricPrompt] issues.
|
||||||
|
* The device lists are taken from [this repository](https://github.com/sergeykomlach/AdvancedBiometricPromptCompat/), in DevicesWithKnownBugs.kt.
|
||||||
|
*/
|
||||||
|
object DevicePromptCheck {
|
||||||
|
|
||||||
|
private val onePlusModelsWithWorkingBiometricUI = setOf(
|
||||||
|
"A0001", // OnePlus One
|
||||||
|
"ONE A2001", "ONE A2003", "ONE A2005", // OnePlus 2
|
||||||
|
"ONE E1001", "ONE E1003", "ONE E1005", // OnePlus X
|
||||||
|
"ONEPLUS A3000", "ONEPLUS SM-A3000", "ONEPLUS A3003", // OnePlus 3
|
||||||
|
"ONEPLUS A3010", // OnePlus 3T
|
||||||
|
"ONEPLUS A5000", // OnePlus 5
|
||||||
|
"ONEPLUS A5010", // OnePlus 5T
|
||||||
|
"ONEPLUS A6000", "ONEPLUS A6003", // OnePlus 6
|
||||||
|
)
|
||||||
|
|
||||||
|
private val lgModelsWithoutBiometricUI = setOf(
|
||||||
|
"G810", // G8 ThinQ "G820", G8S ThinQ
|
||||||
|
"G850", // G8X ThinQ
|
||||||
|
"G900", // Velvet/Velvet 5G
|
||||||
|
"G910", // Velvet 4G Dual Sim
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New OnePlus devices have a bug that prevents the system biometric UI from appearing, only the under display fingerprint is shown.
|
||||||
|
* See [this OP forum thread](https://forums.oneplus.com/threads/oneplus-7-pro-fingerprint-biometricprompt-does-not-show.1035821/).
|
||||||
|
*/
|
||||||
|
private val isOnePlusDeviceWithNoBiometricUI: Boolean =
|
||||||
|
Build.BRAND.equals("OnePlus", ignoreCase = true) &&
|
||||||
|
!onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) &&
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some LG models don't seem to have a system biometric prompt at all.
|
||||||
|
*/
|
||||||
|
private val isLGDeviceWithNoBiometricUI: Boolean =
|
||||||
|
Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this device is included in the list of devices with known Biometric Prompt issues.
|
||||||
|
*/
|
||||||
|
val isDeviceWithNoBiometricUI: Boolean = isOnePlusDeviceWithNoBiometricUI || isLGDeviceWithNoBiometricUI
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the [Int] value contains the provided bit [flag].
|
||||||
|
*/
|
||||||
|
internal fun Int.hasFlag(flag: Int) = this and flag == flag
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.pin.lockscreen.views
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.setMargins
|
||||||
|
import im.vector.app.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom view representing the entered digits of a PIN code screen.
|
||||||
|
*/
|
||||||
|
class LockScreenCodeView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
defStyleRes: Int = 0,
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
private val code: MutableList<Char> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of digits entered.
|
||||||
|
*/
|
||||||
|
val enteredDigits: Int get() = code.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback called when the PIN code has been completely entered.
|
||||||
|
*/
|
||||||
|
var onCodeCompleted: CodeCompletedListener? = null
|
||||||
|
|
||||||
|
var codeLength: Int = 0
|
||||||
|
set(value) {
|
||||||
|
if (value == field) return
|
||||||
|
field = value
|
||||||
|
setupCodeViews()
|
||||||
|
code.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
isSaveEnabled = true
|
||||||
|
|
||||||
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT).also { it.gravity = Gravity.CENTER_HORIZONTAL }
|
||||||
|
orientation = HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_HORIZONTAL
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
private fun setupCodeViews() {
|
||||||
|
removeAllViews()
|
||||||
|
val inflater = LayoutInflater.from(context)
|
||||||
|
repeat(codeLength) { index ->
|
||||||
|
val checkBox = inflater.inflate(R.layout.view_code_checkbox, null) as CheckBox
|
||||||
|
val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
|
val margin = resources.getDimensionPixelSize(R.dimen.lockscreen_code_margin)
|
||||||
|
params.setMargins(margin)
|
||||||
|
checkBox.layoutParams = params
|
||||||
|
checkBox.isChecked = code.size > index
|
||||||
|
addView(checkBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCodeView(index: Int): CheckBox? = getChildAt(index) as? CheckBox
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new [character] to the PIN code. Once it reaches the [codeLength] needed it will invoke the [onCodeCompleted] callback.
|
||||||
|
*/
|
||||||
|
fun onCharInput(character: Char): Int {
|
||||||
|
if (code.size == codeLength) return code.size
|
||||||
|
getCodeView(code.size)?.toggle()
|
||||||
|
code.add(character)
|
||||||
|
if (code.size == codeLength) {
|
||||||
|
onCodeCompleted?.onCodeCompleted(String(code.toCharArray()))
|
||||||
|
}
|
||||||
|
return code.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the last digit in the PIN code if possible.
|
||||||
|
*/
|
||||||
|
fun deleteLast(): Int {
|
||||||
|
if (code.size == 0) return code.size
|
||||||
|
code.removeLast()
|
||||||
|
getCodeView(code.size)?.toggle()
|
||||||
|
return code.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all digits in the PIN code.
|
||||||
|
*/
|
||||||
|
fun clearCode() {
|
||||||
|
code.clear()
|
||||||
|
repeat(codeLength) { getCodeView(it)?.isChecked = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(): Parcelable {
|
||||||
|
return SavedState(super.onSaveInstanceState()!!).also {
|
||||||
|
it.code = code
|
||||||
|
it.codeLength = codeLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||||
|
if (state is SavedState) {
|
||||||
|
codeLength = state.codeLength
|
||||||
|
code.addAll(state.code)
|
||||||
|
}
|
||||||
|
super.onRestoreInstanceState(state)
|
||||||
|
setupCodeViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to listen to when [LockScreenCodeView] receives a whole PIN code.
|
||||||
|
*/
|
||||||
|
fun interface CodeCompletedListener {
|
||||||
|
fun onCodeCompleted(code: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SavedState : BaseSavedState {
|
||||||
|
var code: MutableList<Char> = mutableListOf()
|
||||||
|
var codeLength: Int = 0
|
||||||
|
|
||||||
|
constructor(source: Parcel) : super(source) {
|
||||||
|
source.readList(code, null)
|
||||||
|
codeLength = source.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(superState: Parcelable) : super(superState)
|
||||||
|
|
||||||
|
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(out, flags)
|
||||||
|
out.writeList(code)
|
||||||
|
out.writeInt(codeLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Parcelable.Creator<SavedState> {
|
||||||
|
override fun createFromParcel(source: Parcel): SavedState {
|
||||||
|
return SavedState(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<SavedState?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -180,7 +180,7 @@ class VectorPreferences @Inject constructor(
|
||||||
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
|
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
|
||||||
const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
|
||||||
const val SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG = "SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG"
|
const val SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG = "SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG"
|
||||||
private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG"
|
const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG"
|
||||||
private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG"
|
private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG"
|
||||||
const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
|
const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
|
||||||
|
|
||||||
|
@ -945,6 +945,10 @@ class VectorPreferences @Inject constructor(
|
||||||
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false)
|
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setUseBiometricToUnlock(value: Boolean) {
|
||||||
|
defaultPrefs.edit { putBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, value) }
|
||||||
|
}
|
||||||
|
|
||||||
fun useBiometricsToUnlock(): Boolean {
|
fun useBiometricsToUnlock(): Boolean {
|
||||||
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, true)
|
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,17 +22,23 @@ import androidx.preference.SwitchPreference
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.preference.VectorPreference
|
import im.vector.app.core.preference.VectorPreference
|
||||||
|
import im.vector.app.core.utils.toast
|
||||||
import im.vector.app.features.navigation.Navigator
|
import im.vector.app.features.navigation.Navigator
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.app.features.pin.PinCodeStore
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
import im.vector.app.features.pin.PinMode
|
import im.vector.app.features.pin.PinMode
|
||||||
|
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class VectorSettingsPinFragment @Inject constructor(
|
class VectorSettingsPinFragment @Inject constructor(
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
private val navigator: Navigator,
|
private val navigator: Navigator,
|
||||||
private val notificationDrawerManager: NotificationDrawerManager
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
|
private val biometricHelper: BiometricHelper,
|
||||||
) : VectorSettingsBaseFragment() {
|
) : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
override var titleRes = R.string.settings_security_application_protection_screen_title
|
override var titleRes = R.string.settings_security_application_protection_screen_title
|
||||||
|
@ -50,14 +56,67 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!!
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val useBiometricPref by lazy {
|
||||||
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_BIOMETRICS_FLAG)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldCheckBiometricPref(isPinCodeChecked: Boolean): Boolean {
|
||||||
|
return isPinCodeChecked && // Biometric auth depends on PIN auth
|
||||||
|
biometricHelper.isSystemAuthEnabledAndValid &&
|
||||||
|
biometricHelper.isSystemKeyValid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
useBiometricPref.isEnabled = usePinCodePref.isChecked
|
||||||
|
useBiometricPref.isChecked = shouldCheckBiometricPref(usePinCodePref.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
override fun bindPref() {
|
override fun bindPref() {
|
||||||
refreshPinCodeStatus()
|
refreshPinCodeStatus()
|
||||||
|
|
||||||
|
usePinCodePref.setOnPreferenceChangeListener { _, value ->
|
||||||
|
val isChecked = (value as? Boolean).orFalse()
|
||||||
|
useBiometricPref.isEnabled = isChecked
|
||||||
|
useBiometricPref.isChecked = shouldCheckBiometricPref(isChecked)
|
||||||
|
if (!isChecked) {
|
||||||
|
disableBiometricAuthentication()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ ->
|
||||||
// Refresh the drawer for an immediate effect of this change
|
// Refresh the drawer for an immediate effect of this change
|
||||||
notificationDrawerManager.notificationStyleChanged()
|
notificationDrawerManager.notificationStyleChanged()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useBiometricPref.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
if (newValue as? Boolean == true) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
runCatching {
|
||||||
|
// If previous system key existed, delete it
|
||||||
|
if (biometricHelper.hasSystemKey) {
|
||||||
|
biometricHelper.disableAuthentication()
|
||||||
|
}
|
||||||
|
biometricHelper.enableAuthentication(requireActivity()).collect()
|
||||||
|
}.onFailure {
|
||||||
|
showEnableBiometricErrorMessage()
|
||||||
|
}
|
||||||
|
useBiometricPref.isChecked = shouldCheckBiometricPref(usePinCodePref.isChecked)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
disableBiometricAuthentication()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableBiometricAuthentication() {
|
||||||
|
runCatching { biometricHelper.disableAuthentication() }
|
||||||
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshPinCodeStatus() {
|
private fun refreshPinCodeStatus() {
|
||||||
|
@ -67,7 +126,7 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
if (hasPinCode) {
|
if (hasPinCode) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
pinCodeStore.deleteEncodedPin()
|
pinCodeStore.deletePinCode()
|
||||||
refreshPinCodeStatus()
|
refreshPinCodeStatus()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,6 +152,10 @@ class VectorSettingsPinFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showEnableBiometricErrorMessage() {
|
||||||
|
context?.toast(R.string.settings_security_pin_code_use_biometrics_error)
|
||||||
|
}
|
||||||
|
|
||||||
private val pinActivityResultLauncher = registerStartForActivityResult {
|
private val pinActivityResultLauncher = registerStartForActivityResult {
|
||||||
refreshPinCodeStatus()
|
refreshPinCodeStatus()
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.auth.ReAuthActivity
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
@ -45,7 +46,8 @@ data class DeactivateAccountViewState(
|
||||||
|
|
||||||
class DeactivateAccountViewModel @AssistedInject constructor(
|
class DeactivateAccountViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: DeactivateAccountViewState,
|
@Assisted private val initialState: DeactivateAccountViewState,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val matrix: Matrix,
|
||||||
) :
|
) :
|
||||||
VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
|
VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@ -71,7 +73,7 @@ class DeactivateAccountViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
is DeactivateAccountAction.PasswordAuthDone -> {
|
is DeactivateAccountAction.PasswordAuthDone -> {
|
||||||
_viewEvents.post(DeactivateAccountViewEvents.Loading())
|
_viewEvents.post(DeactivateAccountViewEvents.Loading())
|
||||||
val decryptedPass = session.secureStorageService()
|
val decryptedPass = matrix.secureStorageService()
|
||||||
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
uiaContinuation?.resume(
|
uiaContinuation?.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.app.features.login.ReAuthHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
@ -50,7 +51,8 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: CrossSigningSettingsViewState,
|
@Assisted private val initialState: CrossSigningSettingsViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val reAuthHelper: ReAuthHelper,
|
private val reAuthHelper: ReAuthHelper,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -132,7 +134,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CrossSigningSettingsAction.PasswordAuthDone -> {
|
is CrossSigningSettingsAction.PasswordAuthDone -> {
|
||||||
val decryptedPass = session.secureStorageService()
|
val decryptedPass = matrix.secureStorageService()
|
||||||
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
uiaContinuation?.resume(
|
uiaContinuation?.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
|
|
@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
@ -90,7 +91,8 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: DevicesViewState,
|
@Assisted initialState: DevicesViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val reAuthHelper: ReAuthHelper,
|
private val reAuthHelper: ReAuthHelper,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||||
|
|
||||||
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
@ -219,7 +221,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
is DevicesAction.PasswordAuthDone -> {
|
is DevicesAction.PasswordAuthDone -> {
|
||||||
val decryptedPass = session.secureStorageService()
|
val decryptedPass = matrix.secureStorageService()
|
||||||
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
uiaContinuation?.resume(
|
uiaContinuation?.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.ReadOnceTrue
|
import im.vector.app.core.utils.ReadOnceTrue
|
||||||
import im.vector.app.features.auth.ReAuthActivity
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
@ -47,7 +48,8 @@ import kotlin.coroutines.resumeWithException
|
||||||
class ThreePidsSettingsViewModel @AssistedInject constructor(
|
class ThreePidsSettingsViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: ThreePidsSettingsViewState,
|
@Assisted initialState: ThreePidsSettingsViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider,
|
||||||
|
private val matrix: Matrix,
|
||||||
) : VectorViewModel<ThreePidsSettingsViewState, ThreePidsSettingsAction, ThreePidsSettingsViewEvents>(initialState) {
|
) : VectorViewModel<ThreePidsSettingsViewState, ThreePidsSettingsAction, ThreePidsSettingsViewEvents>(initialState) {
|
||||||
|
|
||||||
// UIA session
|
// UIA session
|
||||||
|
@ -133,7 +135,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ThreePidsSettingsAction.PasswordAuthDone -> {
|
is ThreePidsSettingsAction.PasswordAuthDone -> {
|
||||||
val decryptedPass = session.secureStorageService()
|
val decryptedPass = matrix.secureStorageService()
|
||||||
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
uiaContinuation?.resume(
|
uiaContinuation?.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
|
|
@ -54,7 +54,7 @@ class CreateSpaceAdd3pidInvitesFragment @Inject constructor(
|
||||||
invalidateState(it)
|
invalidateState(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.nextButton.setText(R.string.next_pf)
|
views.nextButton.setText(R.string.action_next)
|
||||||
views.nextButton.debouncedClicks {
|
views.nextButton.debouncedClicks {
|
||||||
view.hideKeyboard()
|
view.hideKeyboard()
|
||||||
sharedViewModel.handle(CreateSpaceAction.NextFromAdd3pid)
|
sharedViewModel.handle(CreateSpaceAction.NextFromAdd3pid)
|
||||||
|
@ -67,7 +67,7 @@ class CreateSpaceAdd3pidInvitesFragment @Inject constructor(
|
||||||
views.nextButton.text = if (noEmails) {
|
views.nextButton.text = if (noEmails) {
|
||||||
getString(R.string.skip_for_now)
|
getString(R.string.skip_for_now)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.next_pf)
|
getString(R.string.action_next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:fromXDelta="0"
|
||||||
|
android:toXDelta="10"
|
||||||
|
android:duration="500"
|
||||||
|
android:interpolator="@anim/lockscreen_shake_interpolator" />
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:cycles="7" />
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2015 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="40.0dp"
|
||||||
|
android:height="40.0dp"
|
||||||
|
android:viewportWidth="40.0"
|
||||||
|
android:viewportHeight="40.0">
|
||||||
|
<path
|
||||||
|
android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z"
|
||||||
|
android:fillColor="#F4511E"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue