Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CMP Mifos Passcode Module Setup #1762

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions libs/cmp-mifos-passcode/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
// alias(libs.plugins.kotlinCocoapods)
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}

kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "11"
}
}
}

jvm("desktop")

listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "cmp-mifos-passcode"
isStatic = true
}
}

// cocoapods {
// summary = "Some description for the Shared Module"
// homepage = "Link to the Shared Module homepage"
// version = "1.0"
// ios.deploymentTarget = "16.0"
// framework {
// baseName = "shared"
// isStatic = true
// }
// }

sourceSets {
commonMain.dependencies {
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(compose.ui)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(libs.navigation.compose)
implementation(libs.multiplatform.settings.no.arg)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
androidMain.dependencies {
implementation (libs.androidx.biometric)
}
}
tasks.register("testClasses")
}

android {
namespace = "com.mifos.passcode"
compileSdk = 35
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.ui)
}

compose.resources {
publicResClass = true
packageOfResClass = "com.mifos.passcode.resources"
generateResClass = always
}
54 changes: 54 additions & 0 deletions libs/cmp-mifos-passcode/shared.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Pod::Spec.new do |spec|
spec.name = 'shared'
spec.version = '1.0'
spec.homepage = 'Link to the Shared Module homepage'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for the Shared Module'
spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '16.0'


if !Dir.exist?('build/cocoapods/framework/shared.framework') || Dir.empty?('build/cocoapods/framework/shared.framework')
raise "

Kotlin framework 'shared' doesn't exist yet, so a proper Xcode project can't be generated.
'pod install' should be executed after running ':generateDummyFramework' Gradle task:

./gradlew :shared:generateDummyFramework

Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
end

spec.xcconfig = {
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
}

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':shared',
'PRODUCT_MODULE_NAME' => 'shared',
}

spec.script_phases = [
{
:name => 'Build shared',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
spec.resources = ['build/compose/cocoapods/compose-resources']
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.mifos.passcode

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.mifos.passcode.utility.AuthenticationResult
import com.mifos.passcode.utility.BioMetricUtil
import java.util.Base64
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class BiometricUtilAndroidImpl(
private val activity: FragmentActivity,
private val cipherUtil: com.mifos.passcode.ICipherUtil
) : BioMetricUtil {

private val executor = ContextCompat.getMainExecutor(activity)
private var promptInfo: BiometricPrompt.PromptInfo? = null
private var biometricPrompt: BiometricPrompt? = null

@RequiresApi(Build.VERSION_CODES.O)
override suspend fun setAndReturnPublicKey(): String? {
val authenticateResult = authenticate()
return when (authenticateResult) {
is AuthenticationResult.Success -> generatePublicKey()
else -> null
}
}

override fun canAuthenticate(): Boolean {
return BiometricManager.from(activity).canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
}

@RequiresApi(Build.VERSION_CODES.O)
override fun generatePublicKey(): String? {
return cipherUtil.generateKeyPair().public?.encoded?.toBase64Encoded()?.toPemFormat()?.toBase64Encoded()
}

@RequiresApi(Build.VERSION_CODES.O)
override fun getPublicKey(): String? {
return cipherUtil.getPublicKey()?.encoded?.toBase64Encoded()?.toPemFormat()?.toBase64Encoded()
}

override fun isValidCrypto(): Boolean {
return try {
cipherUtil.getCrypto()
true
} catch (e: Exception){
false
}
}

override suspend fun authenticate(): AuthenticationResult = suspendCoroutine { continuation ->

biometricPrompt = BiometricPrompt(activity, executor, object :
BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> continuation.resume(
AuthenticationResult.AttemptExhausted)
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> continuation.resume(
AuthenticationResult.NegativeButtonClick)
else -> continuation.resume(AuthenticationResult.Error(errString.toString()))
}
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
continuation.resume(AuthenticationResult.Success)
}
})

promptInfo?.let {
biometricPrompt?.authenticate(it, cipherUtil.getCrypto())
}
}

@RequiresApi(Build.VERSION_CODES.O)
override fun signUserId(ucc: String): String {
cipherUtil.getCrypto().signature?.update(ucc.toByteArray())
return cipherUtil.getCrypto().signature?.sign()?.toBase64Encoded() ?: ""
}

@RequiresApi(Build.VERSION_CODES.O)
override fun isBiometricSet(): Boolean {
return !getPublicKey().isNullOrEmpty() && isValidCrypto()
}

fun preparePrompt(
title: String,
subtitle: String,
description: String,
): BioMetricUtil {
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BIOMETRIC_STRONG)
.build()
return this
}
}

@RequiresApi(Build.VERSION_CODES.O)
fun ByteArray.toBase64Encoded(): String? {
return Base64.getEncoder().encodeToString(this)
}

@RequiresApi(Build.VERSION_CODES.O)
fun String.toBase64Encoded(): String? {
return Base64.getEncoder().encodeToString(this.toByteArray())
}

private fun String.toPemFormat(): String {
val stringBuilder = StringBuilder()
stringBuilder.append("-----BEGIN RSA PUBLIC KEY-----").append("\n")
chunked(64).forEach {
stringBuilder.append(it).append("\n")
}
stringBuilder.append("-----END RSA PUBLIC KEY-----")
return stringBuilder.toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mifos.passcode

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.biometric.BiometricPrompt
import com.mifos.passcode.ICipherUtil
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Signature

class CipherUtilAndroidImpl: com.mifos.passcode.ICipherUtil {
private val KEY_NAME = "biometric_key"

override fun generateKeyPair(): KeyPair {
val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY).run {
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
build()
}
keyPairGenerator.initialize(parameterSpec)
return keyPairGenerator.genKeyPair()
}

override fun getPublicKey(): PublicKey? = getKeyPair()?.public

private fun getKeyPair(): KeyPair? {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore?.getCertificate(KEY_NAME).let { return KeyPair(it?.publicKey, null) }
}

override fun getCrypto(): Crypto {
val signature = Signature.getInstance("SHA256withRSA")
val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val key: PrivateKey = if(keyStore.containsAlias(KEY_NAME))
keyStore.getKey(KEY_NAME, null) as PrivateKey
else
generateKeyPair().private
signature.initSign(key)
return BiometricPrompt.CryptoObject(signature)
}

override suspend fun removePublicKey() {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore?.deleteEntry(KEY_NAME)
}

}

actual typealias CommonKeyPair = KeyPair

actual typealias CommonPublicKey = PublicKey

actual typealias Crypto = BiometricPrompt.CryptoObject
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mifos.passcode

import com.mifos.passcode.Platform

class AndroidPlatform : com.mifos.passcode.Platform {
// override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
override val name: String = "Android"
}

actual fun getPlatform(): com.mifos.passcode.Platform = AndroidPlatform()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package com.mifos.passcode

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z"/>

</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_name">Biometric</string>
<string name="pref_name">Passcode</string>
<string name="has_passcode">hasPasscode</string>
<string name="has_drag_passcode">hasDragPasscode</string>
<string name="passcode">passcode</string>
<string name="drag_passcode">drag_passcode</string>
<string name="create_passcode">Create Passcode</string>
<string name="confirm_passcode">Confirm Passcode</string>
<string name="enter_your_passcode">Enter your Passcode</string>
<string name="forgot_passcode">Forgot Passcode</string>
<string name="delete_passcode">Delete Passcode Key Button</string>
<string name="drag_guide">Drag your finger here only in one direction.</string>
<string name="drag_your_pattern">Drag your Pattern</string>
<string name="exit">Exit</string>
<string name="cancel">Cancel</string>
<string name="skip">Skip</string>
<string name="forgot_passcode_login_manually">Forgot Passcode, Login Manually</string>
<string name="try_again">Try again</string>
<string name="passcode_do_not_match">Passcode do not match!</string>
<string name="are_you_sure_you_want_to_exit">Are you sure you want to exit?</string>
<string name="use_touchId">Use TouchId</string>
<string name="use_faceId">Use FaceId</string>h
<string name="authentication_failed">Authentication failed</string>
<string name="authentication_not_set">Authentication not set</string>
<string name="authentication_not_available">Feature unavailable</string>
<string name="biometric_registration_success">Biometric Registration Successful !</string>
<string name="enable_biometric_dialog_title">Do you want to enable app lock ?</string>
<string name="enable_biometric_dialog_description">Use your existing PIN, pattern, face ID, or fingerprint to unlock this app.</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="ok">Ok</string>
</resources>
Loading