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 2 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
10 changes: 10 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
accompanistPagerVersion = "0.34.0"
activityVersion = "1.9.1"
agp = "8.3.0"
androidDesugarJdkLibs = "2.0.4"
androidGradlePlugin = "8.5.2"
androidTools = "31.5.2"
Expand All @@ -23,6 +24,7 @@ androidxNavigation = "2.8.0-rc01"
androidxProfileinstaller = "1.3.1"
androidxTracing = "1.3.0-alpha02"
appcompatVersion = "1.7.0"
biometricKtx = "1.1.0"
cameraLifecycleVersion = "1.3.4"
cameraViewVersion = "1.3.4"
coil = "2.6.0"
Expand Down Expand Up @@ -58,9 +60,12 @@ ktorVersion = "2.3.4"
libphonenumberAndroidVersion = "8.13.35"
lifecycleExtensionsVersion = "2.2.0"
lifecycleVersion = "2.8.4"
lifecycleViewmodelKtx = "2.8.1"
logbackClassicVersion = "1.2.3"
minSdk = "24"
moduleGraph = "2.5.0"
multiplatformSettings = "1.0.0"
navigationComposeVersion = "2.7.0-alpha07"
okHttp3Version = "4.12.0"
playServicesAuthVersion = "21.2.0"
playServicesCodeScanner = "16.1.0"
Expand Down Expand Up @@ -90,6 +95,7 @@ android-tools-common = { group = "com.android.tools", name = "common", version.r
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityVersion" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" }
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometricKtx" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycleVersion" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraViewVersion" }
Expand Down Expand Up @@ -123,6 +129,7 @@ androidx-lifecycle-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewm
androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycleViewmodelKtx" }
androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
Expand Down Expand Up @@ -159,6 +166,7 @@ koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmo
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
Expand All @@ -180,6 +188,8 @@ lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref =
lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "androidTools" }
lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "androidTools" }
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassicVersion" }
multiplatform-settings-no-arg = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" }
navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "navigationComposeVersion" }
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuthVersion" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
Expand Down
20 changes: 20 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
189117BF2C6CC76200DABAA8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189117BE2C6CC76200DABAA8 /* ContentView.swift */; };
189117C12C6CC76400DABAA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 189117C02C6CC76400DABAA8 /* Assets.xcassets */; };
189117C42C6CC76400DABAA8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 189117C32C6CC76400DABAA8 /* Preview Assets.xcassets */; };
8E096B732C90B3B600C3BEA6 /* BiometricUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */; };
8E096B752C90B3C100C3BEA6 /* CipherUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */; };
8EAA94DE2C92A681005081E9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8EAA94DD2C92A681005081E9 /* Info.plist */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -19,6 +22,9 @@
189117BE2C6CC76200DABAA8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
189117C02C6CC76400DABAA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
189117C32C6CC76400DABAA8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricUtil.swift; sourceTree = "<group>"; };
8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherUtil.swift; sourceTree = "<group>"; };
8EAA94DD2C92A681005081E9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -51,7 +57,9 @@
189117BB2C6CC76200DABAA8 /* iosApp */ = {
isa = PBXGroup;
children = (
8E096B712C90B3A000C3BEA6 /* Biometric */,
189117BC2C6CC76200DABAA8 /* iosAppApp.swift */,
8EAA94DD2C92A681005081E9 /* Info.plist */,
189117BE2C6CC76200DABAA8 /* ContentView.swift */,
189117C02C6CC76400DABAA8 /* Assets.xcassets */,
189117C22C6CC76400DABAA8 /* Preview Content */,
Expand All @@ -67,6 +75,15 @@
path = "Preview Content";
sourceTree = "<group>";
};
8E096B712C90B3A000C3BEA6 /* Biometric */ = {
isa = PBXGroup;
children = (
8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */,
8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */,
);
path = Biometric;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -128,6 +145,7 @@
files = (
189117C42C6CC76400DABAA8 /* Preview Assets.xcassets in Resources */,
189117C12C6CC76400DABAA8 /* Assets.xcassets in Resources */,
8EAA94DE2C92A681005081E9 /* Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -158,6 +176,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8E096B752C90B3C100C3BEA6 /* CipherUtil.swift in Sources */,
8E096B732C90B3B600C3BEA6 /* BiometricUtil.swift in Sources */,
189117BF2C6CC76200DABAA8 /* ContentView.swift in Sources */,
189117BD2C6CC76200DABAA8 /* iosAppApp.swift in Sources */,
);
Expand Down
112 changes: 112 additions & 0 deletions iosApp/iosApp/Biometric/BiometricUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Foundation
import shared
import LocalAuthentication

class BiometricUtilIosImpl: BioMetricUtil {

private let cipherUtil = CipherUtilIosImpl()

private var promptDescription: String = "Authenticate"

func setAndReturnPublicKey(completionHandler: @escaping (String?, (any Error)?) -> Void) {

let laContext = LAContext()
laContext.localizedReason = promptDescription
laContext.localizedFallbackTitle = "Cancel"
laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: promptDescription) {
[weak self] success, authenticationError in

DispatchQueue.main.async {
if success {
completionHandler(self?.generatePublicKey(), nil)
} else {
completionHandler(nil, authenticationError)
}
}
}
}

func authenticate() async throws -> AuthenticationResult {
do {
_ = try self.cipherUtil.getCrypto()
return AuthenticationResult.Success()
} catch {
print("AuthenticateError: \(error.localizedDescription)")
return AuthenticationResult.Error(error: error.localizedDescription)
}
}

func canAuthenticate() -> Bool {
var error: NSError?
let laContext = LAContext()
return laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
}


func generatePublicKey() -> String? {
let keyPair = try? cipherUtil.generateKeyPair()
return keyPair?.publicKey?.toPemFormat().toBase64()
}

func getPublicKey() -> String? {
return cipherUtil.getPublicKey()?.encoded?.toPemFormat().toBase64()
}

func isBiometricSet() -> Bool {
return (getPublicKey() != nil) && isValidCrypto()
}

func isValidCrypto() -> Bool {
do {
_ = try cipherUtil.getCrypto()
return true
} catch {
return false
}
}

func signUserId(ucc: String) -> String {
guard let data = ucc.data(using: .utf8) else {
print("Failed to convert UCC to data")
fatalError()
}

var error: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(cipherUtil.getKey()!, .rsaSignatureMessagePKCS1v15SHA256, data as CFData, &error) else {
if let error = error {
print("Error creating signature: \(error.takeRetainedValue())")
}
fatalError()
}

return (signature as Data).base64EncodedString()
}


}

extension String {
func toPemFormat() -> String {
let chunkSize = 64
var pemString = "-----BEGIN RSA PUBLIC KEY-----\n"
var base64String = self
while base64String.count > 0 {
let chunkIndex = base64String.index(base64String.startIndex, offsetBy: min(chunkSize, base64String.count))
let chunk = base64String[..<chunkIndex]
pemString.append(contentsOf: chunk)
pemString.append("\n")
base64String = String(base64String[chunkIndex...])
}
pemString.append("-----END RSA PUBLIC KEY-----")
return pemString
}
}

extension String {
func toBase64() -> String? {
guard let data = self.data(using: .utf8) else {
return nil
}
return data.base64EncodedString()
}
}
102 changes: 102 additions & 0 deletions iosApp/iosApp/Biometric/CipherUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation
import shared

class CipherUtilIosImpl: ICipherUtil {
private let KEY_NAME = "my_biometric_key"
private let tag: Data

private lazy var key: SecKey? = {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnRef as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else { return nil }
return (item as! SecKey)
}()

init() {
self.tag = KEY_NAME.data(using: .utf8)!
}

func generateKeyPair() throws -> CommonKeyPair {
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.userPresence,
nil
)!
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: 2048,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag,
kSecAttrAccessControl as String: access,
]
]

var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Error
}
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecInternalError), userInfo: nil)
}

let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data
let privateKeyData = SecKeyCopyExternalRepresentation(privateKey, nil)! as Data

return CommonKeyPair(publicKey: publicKeyData.base64EncodedString(), privateKey: privateKeyData.base64EncodedString())
}

func getCrypto() throws -> Crypto {
guard let privateKey = getKey() else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecItemNotFound), userInfo: nil)
}

let publicKey = SecKeyCopyPublicKey(privateKey)!
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data
let privateKeyData = SecKeyCopyExternalRepresentation(privateKey, nil)! as Data

UserDefaults.standard.setValue(publicKeyData.base64EncodedString(), forKey: "PublicKey")

return Crypto()
}

func getPublicKey() -> (any CommonPublicKey)? {
let savedPublicKey = UserDefaults.standard.string(forKey: "PublicKey")
if (savedPublicKey != nil) {
return CommonPublicKeyImpl(encoded: savedPublicKey!)
}

guard let privateKey = getKey() else { return CommonPublicKeyImpl(encoded: "") }
guard let publicKey = SecKeyCopyPublicKey(privateKey) else { return CommonPublicKeyImpl(encoded: "") }
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data
let publicKeyString = publicKeyData.base64EncodedString()
UserDefaults.standard.setValue(publicKeyString, forKey: "PublicKey")
return CommonPublicKeyImpl(encoded: publicKeyString)
}


func removePublicKey() async throws {
UserDefaults.standard.removeObject(forKey: "PublicKey")
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: tag,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil)
}
}

func getKey() -> SecKey? {
return key
}

}
27 changes: 23 additions & 4 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@
import SwiftUI
import shared

let biometricUtil = BiometricUtilIosImpl()
struct ContentView: View {
let greet = Greeting().greet()
@State private var path = NavigationPath()

var body: some View {
Text(Greeting().greet())
.padding()
ZStack {
ComposeViewController()
}
}
}

struct ComposeViewController: UIViewControllerRepresentable {
@StateObject var biometricAuthorizationViewModel: BiometricAuthorizationViewModel = BiometricAuthorizationViewModel()
func makeUIViewController(context: Context) -> UIViewController {
return App_iosKt.MainViewController(bioMetricUtil: biometricUtil, biometricViewModel: biometricAuthorizationViewModel)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}

#Preview {
ContentView()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

extension BiometricAuthorizationViewModel: ObservableObject {}
Loading
Loading