Skip to content

Commit

Permalink
Added authentication for payment transaction and id verification
Browse files Browse the repository at this point in the history
Also updated card art
  • Loading branch information
QZHelen committed Nov 20, 2024
1 parent 6fdfcc7 commit aac1485
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 40 deletions.
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<activity
android:name=".GetCredentialActivity"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="androidx.credentials.registry.provider.action.GET_CREDENTIAL" />
<action android:name="androidx.identitycredentials.action.GET_CREDENTIALS" />
Expand Down
101 changes: 84 additions & 17 deletions app/src/main/java/com/credman/cmwallet/GetCredentialActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.credentials.DigitalCredential
import androidx.credentials.ExperimentalDigitalCredentialApi
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetDigitalCredentialOption
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.registry.provider.selectedEntryId
import androidx.fragment.app.FragmentActivity
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import com.credman.cmwallet.data.model.MdocCredential
import com.credman.cmwallet.mdoc.createSessionTranscript
import com.credman.cmwallet.mdoc.filterIssuerSigned
Expand All @@ -20,13 +24,14 @@ import com.credman.cmwallet.openid4vp.OpenId4VP
import com.credman.cmwallet.openid4vp.OpenId4VPMatchedMDocClaims
import org.json.JSONObject

class GetCredentialActivity : ComponentActivity() {
class GetCredentialActivity : FragmentActivity() {
@OptIn(ExperimentalDigitalCredentialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
var shouldFinishActivity = true
if (request != null) {
Log.i("GetCredentialActivity", "selectedEntryId ${request.selectedEntryId}")
Log.i(TAG, "selectedEntryId ${request.selectedEntryId}")
val selectedEntryId = JSONObject(request.selectedEntryId)
val origin = request.callingAppInfo.getOrigin(
CmWalletApplication.credentialRepo.privAppsJson
Expand All @@ -35,10 +40,11 @@ class GetCredentialActivity : ComponentActivity() {

request.credentialOptions.forEach {
if (it is GetDigitalCredentialOption) {
Log.i("GetCredentialActivity", "Request ${it.requestJson}")
Log.i(TAG, "Request ${it.requestJson}")
val providerIdx = selectedEntryId.getInt("provider_idx")
val selectedId = selectedEntryId.getString("id")
val result = Intent()
val resultData = Intent()
shouldFinishActivity = false
try {
val response = processDigitalCredentialOption(
it.requestJson,
Expand All @@ -47,34 +53,85 @@ class GetCredentialActivity : ComponentActivity() {
origin
)

PendingIntentHandler.setGetCredentialResponse(
result, GetCredentialResponse(
DigitalCredential(response)
)
val biometricPrompt = BiometricPrompt(
this@GetCredentialActivity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d(TAG, "onAuthenticationFailed")
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.d(TAG, "onAuthenticationSucceeded")

PendingIntentHandler.setGetCredentialResponse(
resultData, GetCredentialResponse(
DigitalCredential(response.responseJson)
)
)

setResult(RESULT_OK, resultData)
finish()
}

override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
Log.e(TAG, "onAuthenticationError $errorCode $errString")
PendingIntentHandler.setGetCredentialException(
resultData,
GetCredentialUnknownException()
)
setResult(RESULT_OK, resultData)
finish()
}
})
Log.d(TAG, "authenticating")
biometricPrompt.authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(response.authenticationTitle)
.setSubtitle(response.authenticationSubtitle)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
)

} catch (e: Exception) {
Log.e("GetCredentialActivity", "exception", e)
Log.e(TAG, "exception", e)
PendingIntentHandler.setGetCredentialException(
result,
resultData,
GetCredentialUnknownException()
)
setResult(RESULT_OK, resultData)
finish()
}
setResult(RESULT_OK, result)
}
}
}
finish()
if (shouldFinishActivity) {
finish()
}
}

private data class DigitalCredentialResult(
val responseJson: String,
val authenticationTitle: CharSequence,
val authenticationSubtitle: CharSequence?
)

private fun processDigitalCredentialOption(
requestJson: String,
providerIdx: Int,
selectedID: String,
origin: String
): String {
): DigitalCredentialResult {
val selectedCredential = CmWalletApplication.credentialRepo.getCredential(selectedID)
?: throw RuntimeException("Selected credential not found")
var authenticationTitle: CharSequence = "Verify your identity"
var authenticationSubtitle: CharSequence? = null

val request = JSONObject(requestJson)
require(request.has("providers")) { "DigitalCredentialOption requires providers" }
Expand Down Expand Up @@ -110,12 +167,18 @@ class GetCredentialActivity : ComponentActivity() {
val deviceNamespaces = if (openId4VPRequest.transactionData.isEmpty()) {
emptyMap<String, Any>()
} else {
val deviceSignedTransactionData =
openId4VPRequest.generateDeviceSignedTransactionData(
matchedCredential.dcqlId
)
if (deviceSignedTransactionData.authenticationTitleAndSubtitle != null) {
authenticationTitle = deviceSignedTransactionData.authenticationTitleAndSubtitle.first
authenticationSubtitle = deviceSignedTransactionData.authenticationTitleAndSubtitle.second
}
mapOf(
Pair(
"net.openid.open4vc",
openId4VPRequest.generateDeviceSignedTransactionData(
matchedCredential.dcqlId
)
deviceSignedTransactionData.deviceSignedTransactionData
)
)
}
Expand All @@ -140,7 +203,11 @@ class GetCredentialActivity : ComponentActivity() {
// Create the openid4vp result
val responseJson = JSONObject()
responseJson.put("vp_token", vpToken)
return responseJson.toString()
return DigitalCredentialResult(
responseJson = responseJson.toString(),
authenticationTitle = authenticationTitle,
authenticationSubtitle = authenticationSubtitle,
)

}

Expand Down
35 changes: 29 additions & 6 deletions app/src/main/java/com/credman/cmwallet/openid4vp/OpenId4VP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,39 @@ class OpenId4VP(val request: String) {

}

fun generateDeviceSignedTransactionData(dcqlId: String): Map<String, List<ByteArray>> {
data class TransactionDataResult(
val deviceSignedTransactionData: Map<String, List<ByteArray>>,
val authenticationTitleAndSubtitle: Pair<CharSequence, CharSequence?>?,
)

fun generateDeviceSignedTransactionData(dcqlId: String): TransactionDataResult {
if (transactionData.isEmpty()) {
return emptyMap()
return TransactionDataResult(emptyMap(), null)
}
val transactionDataHashes = mutableListOf<ByteArray>()
var authenticationTitleAndSubtitle: Pair<CharSequence, CharSequence?>? = null
for (transactionDataItem in transactionData) {
if (dcqlId in transactionDataItem.credentialIds) {
val md = MessageDigest.getInstance("SHA-256")
transactionDataHashes.add(md.digest(transactionDataItem.encodedData.encodeToByteArray()))
val decoded = JSONObject(String(Base64.decode(transactionDataItem.encodedData, Base64.URL_SAFE)))
val merchantName = decoded.optString(MERCHANT_NAME)
val amount = decoded.optString(AMOUNT)
if (!merchantName.isNullOrBlank() && !amount.isNullOrBlank()) {
authenticationTitleAndSubtitle = Pair(
"Confirm transaction",
"Authorize payment of amount $amount to $merchantName.")
}
}
}
return mapOf(Pair(
"transaction_data_hashes",
transactionDataHashes.toList()
))
return TransactionDataResult(
mapOf(
Pair(
"transaction_data_hashes",
transactionDataHashes.toList()
)),
authenticationTitleAndSubtitle,
)
}

fun matchCredentials(credentialStore: JSONObject): Map<String, List<MatchedCredential>> {
Expand All @@ -98,4 +116,9 @@ class OpenId4VP(val request: String) {
md.digest(cborEncode(CborTag(24, cborEncode(handoverData))))
)
}

companion object {
const val MERCHANT_NAME = "merchant_name"
const val AMOUNT = "amount"
}
}
29 changes: 13 additions & 16 deletions app/src/main/java/com/credman/cmwallet/ui/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.credman.cmwallet.R
import com.credman.cmwallet.data.model.Credential
import com.credman.cmwallet.data.model.CredentialItem
import com.credman.cmwallet.data.model.MdocCredential
Expand Down Expand Up @@ -186,33 +189,27 @@ fun CredentialCard(
Box(
Modifier
.fillMaxSize()
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0x407D5280), Color(0x40EFB8C8)
)
)
.paint(
painterResource(id = R.drawable.card_art_dark),
contentScale = ContentScale.Crop,
)
) {
Row() {
Image(
modifier = Modifier
.padding(10.dp)
.size(80.dp, 80.dp),
imageVector = Icons.Filled.Face,
contentDescription = ""
)
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically){
Column(
modifier = Modifier.padding(10.dp, 20.dp)
modifier = Modifier.padding(20.dp, 20.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
text = metadata.title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
fontWeight = FontWeight.Bold,
color = Color.White,
)
Text(
text = metadata.subtitle ?: "",
fontSize = 16.sp,
color = Color.White,
)
}
}
Expand Down
Binary file added app/src/main/res/drawable/card_art_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/card_art_gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/card_art_yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit aac1485

Please sign in to comment.