Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ internal class AppResponseBuilderUseCase @Inject constructor(
request = request,
results = results,
project = project,
enrolmentSubjectId = enrolmentSubjectId
enrolmentSubjectId = enrolmentSubjectId,
)
} else {
handleIdentify(projectConfiguration, results)
handleIdentify(projectConfiguration, project, results)
}

is ActionRequest.IdentifyActionRequest -> handleIdentify(projectConfiguration, results)
is ActionRequest.IdentifyActionRequest -> handleIdentify(projectConfiguration, project, results)
is ActionRequest.VerifyActionRequest -> handleVerify(projectConfiguration, results)
is ActionRequest.ConfirmIdentityActionRequest -> handleConfirmIdentity(results)
is ActionRequest.EnrolLastBiometricActionRequest -> handleEnrolLastBiometric(results)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.simprints.feature.orchestrator.usecases.response

import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential
import com.simprints.infra.config.store.models.DecisionPolicy
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.TokenKeyType
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.matching.FaceMatchResult
import com.simprints.infra.matching.FingerprintMatchResult
Expand All @@ -17,9 +21,11 @@ import javax.inject.Inject

internal class CreateIdentifyResponseUseCase @Inject constructor(
private val eventRepository: SessionEventRepository,
private val tokenizationProcessor: TokenizationProcessor,
) {
suspend operator fun invoke(
projectConfiguration: ProjectConfiguration,
project: Project?,
results: List<Serializable>,
): AppResponse {
val isMultiFactorIdEnabled = projectConfiguration.multifactorId?.allowedExternalCredentials?.isNotEmpty() ?: false
Expand Down Expand Up @@ -51,7 +57,7 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
.filterIsInstance(ExternalCredentialSearchResult::class.java)
.lastOrNull()
?.scannedCredential
.toAppExternalCredential()
?.toAppExternalCredential(tokenizationProcessor, project)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very confused - the PR description is about returning the edited value but the changes are only about tokenising the previously provided value.

Copy link
Contributor Author

@alexandr-simprints alexandr-simprints Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously it was accessing the initially captured value, which is unencrypted: ScannedCredential::scannedValue

Not it is accessing the final credential value, which is encrypted: ScannedCredential::credential

See here

private fun ScannedCredential.toAppExternalCredential( 
    tokenizationProcessor: TokenizationProcessor,
    project: Project?,
): AppExternalCredential? {
    val decryptedValue = tokenizationProcessor.decrypt(
        encrypted = credential, // <--- Here
    )
    // [...]
    return AppExternalCredential(
        id = credentialScanId,
        value = decryptedValue,
        type = credentialType,
    )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by the same. Why aren't we encrypting scannedValue, too? It will be the actual credential in > 99% of cases and having it saved as plain string kind of makes credential encryption pointless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of the scanned value is for evaluation of how off was scanned value from the final value (stored as credential), see here and here

I see why it might be confusing, I have plans to refactor the returning types a bit for easier separation of concerns. Currently, the ScannedCredential is assigned too much responsibilities that arose as a consequence of the changed requirements

Copy link
Contributor

@BurningAXE BurningAXE Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why we'd want both the scanned and the final values. However, encrypting just the final one doesn't make much sense. Is there any reason for this discrepancy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, we have one event that requires both so that we can see how often attendants need to edit ORC results.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question is not why we keep both but why we encrypt only one of them? They are supposed to be the same values in > 99% of cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FSR :D


return AppIdentifyResponse(
sessionId = currentSessionId,
Expand All @@ -61,11 +67,20 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
)
}

private fun ScannedCredential?.toAppExternalCredential(): AppExternalCredential? = this?.let { scannedCredential ->
AppExternalCredential(
id = scannedCredential.credentialScanId,
value = scannedCredential.scannedValue,
type = scannedCredential.credentialType,
private fun ScannedCredential.toAppExternalCredential(
tokenizationProcessor: TokenizationProcessor,
project: Project?,
): AppExternalCredential? {
if (project == null) return null
val decryptedValue = tokenizationProcessor.decrypt(
encrypted = credential,
tokenKeyType = TokenKeyType.ExternalCredential,
project = project,
) as? TokenizableString.Raw ?: return null
return AppExternalCredential(
id = credentialScanId,
value = decryptedValue,
type = credentialType,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal class AppResponseBuilderUseCaseTest {
MockKAnnotations.init(this, relaxUnitFun = true)

coEvery { handleEnrolment.invoke(any(), any(), any(), any()) } returns mockk()
coEvery { handleIdentify.invoke(any(), any()) } returns mockk()
coEvery { handleIdentify.invoke(any(), any(), any()) } returns mockk()
every { handleVerify.invoke(any(), any()) } returns mockk()
every { handleConfirmIdentity.invoke(any()) } returns mockk()
every { handleEnrolLastBiometric.invoke(any()) } returns mockk()
Expand All @@ -67,13 +67,13 @@ internal class AppResponseBuilderUseCaseTest {
fun `Handles as identification for enrolment action with existing item`() = runTest {
every { isNewEnrolment(any(), any()) } returns false
useCase(mockk(), mockk<ActionRequest.EnrolActionRequest>(), mockk(), mockk(), enrolmentSubjectId)
coVerify { handleIdentify.invoke(any(), any()) }
coVerify { handleIdentify.invoke(any(), any(), any()) }
}

@Test
fun `Handles as identification for identification action`() = runTest {
useCase(mockk(), mockk<ActionRequest.IdentifyActionRequest>(), mockk(), mockk(), enrolmentSubjectId)
coVerify { handleIdentify.invoke(any(), any()) }
coVerify { handleIdentify.invoke(any(), any(), any()) }
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.simprints.feature.orchestrator.usecases.response

import com.google.common.truth.Truth.assertThat
import com.simprints.core.domain.externalcredential.ExternalCredentialType
import com.simprints.core.domain.tokenization.asTokenizableRaw
import com.simprints.feature.externalcredential.ExternalCredentialSearchResult
import com.simprints.feature.externalcredential.model.CredentialMatch
import com.simprints.infra.config.store.models.DecisionPolicy
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.config.store.tokenization.TokenizationProcessor
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.matching.FaceMatchResult
import com.simprints.infra.matching.FingerprintMatchResult
Expand All @@ -24,6 +28,12 @@ class CreateIdentifyResponseUseCaseTest {
@MockK
lateinit var eventRepository: SessionEventRepository

@MockK
lateinit var tokenizationProcessor: TokenizationProcessor

@MockK
lateinit var project: Project

private lateinit var useCase: CreateIdentifyResponseUseCase

@Before
Expand All @@ -32,7 +42,7 @@ class CreateIdentifyResponseUseCaseTest {

coEvery { eventRepository.getCurrentSessionScope().id } returns "sessionId"

useCase = CreateIdentifyResponseUseCase(eventRepository)
useCase = CreateIdentifyResponseUseCase(eventRepository, tokenizationProcessor)
}

@Test
Expand All @@ -45,6 +55,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isEmpty()
Expand All @@ -60,6 +71,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -76,6 +88,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(20f, 25f, 30f, 40f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -92,6 +105,7 @@ class CreateIdentifyResponseUseCaseTest {
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(15f, 30f, 100f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -112,6 +126,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(10f, 20f, 30f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -132,6 +147,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(20f, 25f, 30f, 40f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -152,6 +168,7 @@ class CreateIdentifyResponseUseCaseTest {
)
},
results = listOf(createFingerprintMatchResult(15f, 30f, 100f)),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -175,6 +192,7 @@ class CreateIdentifyResponseUseCaseTest {
createFaceMatchResult(15f, 30f, 100f),
createFingerprintMatchResult(15f, 30f, 105f),
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand All @@ -198,6 +216,7 @@ class CreateIdentifyResponseUseCaseTest {
createFaceMatchResult(15f, 30f, 105f),
createFingerprintMatchResult(15f, 30f, 100f),
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand Down Expand Up @@ -262,6 +281,7 @@ class CreateIdentifyResponseUseCaseTest {
every { scannedCredential } returns null
},
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).isNotEmpty()
Expand Down Expand Up @@ -314,6 +334,7 @@ class CreateIdentifyResponseUseCaseTest {
FaceConfiguration.BioSdk.RANK_ONE,
),
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).hasSize(1)
Expand Down Expand Up @@ -366,6 +387,7 @@ class CreateIdentifyResponseUseCaseTest {
FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER,
),
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).hasSize(1)
Expand Down Expand Up @@ -437,6 +459,7 @@ class CreateIdentifyResponseUseCaseTest {
},
faceMatchResults,
),
project = project,
)

assertThat((result as AppIdentifyResponse).identifications).hasSize(maxNbOfReturnedCandidates)
Expand All @@ -454,6 +477,40 @@ class CreateIdentifyResponseUseCaseTest {
)
}

@Test
fun `Returns scanned credential when decryption succeeds`() = runTest {
val id = "id"
val type = ExternalCredentialType.NHISCard
val expectedDecrypted = "expectedDecrypted".asTokenizableRaw()

every { tokenizationProcessor.decrypt(any(), any(), any()) } returns expectedDecrypted

val result = useCase(
mockk {
every { multifactorId?.allowedExternalCredentials } returns null
every { identification.maxNbOfReturnedCandidates } returns 2
every { face?.getSdkConfiguration(any())?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration(any())?.decisionPolicy } returns null
},
results = listOf(
mockk<ExternalCredentialSearchResult> {
every { matchResults } returns emptyList()
every { scannedCredential } returns mockk {
every { credentialScanId } returns id
every { credentialType } returns type
every { credential } returns mockk()
}
},
),
project = project,
)

assertThat((result as AppIdentifyResponse).scannedCredential).isNotNull()
assertThat(result.scannedCredential?.id).isEqualTo(id)
assertThat(result.scannedCredential?.type).isEqualTo(type)
assertThat(result.scannedCredential?.value).isEqualTo(expectedDecrypted)
}

private fun createFaceMatchResult(vararg confidences: Float): Serializable = FaceMatchResult(
confidences.mapIndexed { i, confidence -> FaceMatchResult.Item(subjectId = "$i", confidence = confidence) },
FaceConfiguration.BioSdk.RANK_ONE,
Expand Down