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

Replace ThreadExecutor with Coroutines #416

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 @@ -5,8 +5,6 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color.BLACK
import android.graphics.Color.WHITE
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ImageView
import androidx.lifecycle.LiveData
Expand All @@ -32,8 +30,9 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.*
import java.util.concurrent.Executors
import kotlin.coroutines.resume

class TransferManager private constructor(private val context: Context) {

Expand Down Expand Up @@ -159,90 +158,82 @@ class TransferManager private constructor(private val context: Context) {
}

@Throws(IllegalStateException::class)
fun addDocumentToResponse(
suspend fun addDocumentToResponse(
credentialName: String,
docType: String,
issuerSignedEntriesToRequest: MutableMap<String, Collection<String>>,
deviceResponseGenerator: DeviceResponseGenerator,
authKey: Credential.AuthenticationKey?,
authKeyUnlockData: SecureArea.KeyUnlockData?,
onResultReady: (result: AddDocumentToResponseResult) -> Unit
) {
// Putting together the response involves a bit of work and also potentially
// blocking I/O. Do all this on a worker thread.
Executors.newSingleThreadExecutor().execute(kotlinx.coroutines.Runnable {
var result: AddDocumentToResponseResult
var signingKeyUsageLimitPassed = false
val documentManager = DocumentManager.getInstance(context)
val documentInformation = documentManager.getDocumentInformation(credentialName)
requireValidProperty(documentInformation) { "Document not found!" }
) = suspendCancellableCoroutine { continuation ->
var result: AddDocumentToResponseResult
var signingKeyUsageLimitPassed = false
val documentManager = DocumentManager.getInstance(context)
val documentInformation = documentManager.getDocumentInformation(credentialName)
requireValidProperty(documentInformation) { "Document not found!" }

val credential = requireNotNull(documentManager.getCredentialByName(credentialName))
val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key ->
issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value ->
CredentialRequest.DataElement(key, value, false)
}
val credential = requireNotNull(documentManager.getCredentialByName(credentialName))
val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key ->
issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value ->
CredentialRequest.DataElement(key, value, false)
}
}

val request = CredentialRequest(dataElements)
val request = CredentialRequest(dataElements)

val authKeyToUse : Credential.AuthenticationKey
if (authKey != null) {
authKeyToUse = authKey
} else {
authKeyToUse = credential.findAuthenticationKey(Timestamp.now())
?: throw IllegalStateException("No auth key available")
}
val authKeyToUse: Credential.AuthenticationKey
if (authKey != null) {
authKeyToUse = authKey
} else {
authKeyToUse = credential.findAuthenticationKey(Timestamp.now())
?: throw IllegalStateException("No auth key available")
}

if (authKeyToUse.usageCount >= documentInformation.maxUsagesPerKey) {
logWarning("Using Auth Key previously used ${authKeyToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}")
signingKeyUsageLimitPassed = true
}
if (authKeyToUse.usageCount >= documentInformation.maxUsagesPerKey) {
logWarning("Using Auth Key previously used ${authKeyToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}")
signingKeyUsageLimitPassed = true
}

val staticAuthData = StaticAuthDataParser(authKeyToUse.issuerProvidedData).parse()
val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces(
request,
credential.applicationData.getNameSpacedData("credentialData"),
staticAuthData
)
val staticAuthData = StaticAuthDataParser(authKeyToUse.issuerProvidedData).parse()
val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces(
request,
credential.applicationData.getNameSpacedData("credentialData"),
staticAuthData
)

val transcript = communication.getSessionTranscript() ?: byteArrayOf()
val transcript = communication.getSessionTranscript() ?: byteArrayOf()

try {
val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript)
.setIssuerNamespaces(mergedIssuerNamespaces)
val keyInfo = authKeyToUse.secureArea.getKeyInfo(authKeyToUse.alias)
if ((keyInfo.keyPurposes and SecureArea.KEY_PURPOSE_SIGN) != 0) {
generator.setDeviceNamespacesSignature(
NameSpacedData.Builder().build(),
authKeyToUse.secureArea,
authKeyToUse.alias,
authKeyUnlockData,
SecureArea.ALGORITHM_ES256
)
} else {
generator.setDeviceNamespacesMac(
NameSpacedData.Builder().build(),
authKeyToUse.secureArea,
authKeyToUse.alias,
authKeyUnlockData,
communication.deviceRetrievalHelper!!.eReaderKey
)
}
val data = generator.generate()
deviceResponseGenerator.addDocument(data)
log("Increasing usage count on ${authKeyToUse.alias}")
authKeyToUse.increaseUsageCount()
ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential)
result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed)
} catch (lockedException: SecureArea.KeyLockedException) {
result = AddDocumentToResponseResult.DocumentLocked(authKeyToUse)
}
// Post the result on the UI thread
Handler(Looper.getMainLooper()).post {
onResultReady(result)
try {
val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript)
.setIssuerNamespaces(mergedIssuerNamespaces)
val keyInfo = authKeyToUse.secureArea.getKeyInfo(authKeyToUse.alias)
if ((keyInfo.keyPurposes and SecureArea.KEY_PURPOSE_SIGN) != 0) {
generator.setDeviceNamespacesSignature(
NameSpacedData.Builder().build(),
authKeyToUse.secureArea,
authKeyToUse.alias,
authKeyUnlockData,
SecureArea.ALGORITHM_ES256
)
} else {
generator.setDeviceNamespacesMac(
NameSpacedData.Builder().build(),
authKeyToUse.secureArea,
authKeyToUse.alias,
authKeyUnlockData,
communication.deviceRetrievalHelper!!.eReaderKey
)
}
})
val data = generator.generate()
deviceResponseGenerator.addDocument(data)
log("Increasing usage count on ${authKeyToUse.alias}")
authKeyToUse.increaseUsageCount()
ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential)
result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed)
} catch (lockedException: SecureArea.KeyLockedException) {
result = AddDocumentToResponseResult.DocumentLocked(authKeyToUse)
}
continuation.resume(result)
}

fun stopPresentation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import androidx.databinding.ObservableInt
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK
import androidx.lifecycle.viewModelScope
import com.android.identity.android.legacy.CredentialInvalidatedException
import com.android.identity.credential.Credential
import com.android.identity.mdoc.response.DeviceResponseGenerator
import com.android.identity.mdoc.request.DeviceRequestParser
import com.android.identity.mdoc.response.DeviceResponseGenerator
import com.android.identity.securearea.SecureArea
import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK
import com.android.identity.wallet.R
import com.android.identity.wallet.authconfirmation.RequestedDocumentData
import com.android.identity.wallet.authconfirmation.RequestedElement
Expand All @@ -22,9 +23,12 @@ import com.android.identity.wallet.document.DocumentInformation
import com.android.identity.wallet.document.DocumentManager
import com.android.identity.wallet.transfer.AddDocumentToResponseResult
import com.android.identity.wallet.transfer.TransferManager
import com.android.identity.wallet.util.PreferencesHelper
import com.android.identity.wallet.util.TransferStatus
import com.android.identity.wallet.util.logWarning
import com.android.identity.wallet.util.PreferencesHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) {

Expand Down Expand Up @@ -105,37 +109,44 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) {
) {
val elementsToSend = signedElements.collect()
val responseGenerator = DeviceResponseGenerator(DEVICE_RESPONSE_STATUS_OK)
elementsToSend.forEach { signedDocument ->
try {
val issuerSignedEntries = signedDocument.issuerSignedEntries()
transferManager.addDocumentToResponse(
signedDocument.identityCredentialName,
signedDocument.documentType,
issuerSignedEntries,
responseGenerator,
authKey,
authKeyUnlockData,
onResultReady = {
if (it !is AddDocumentToResponseResult.DocumentAdded) {
onResultReady(it)
return@addDocumentToResponse
}
transferManager.sendResponse(responseGenerator.generate(), PreferencesHelper.isConnectionAutoCloseEnabled())
transferManager.setResponseServed()
val documentsCount = elementsToSend.count()
documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount))
cleanUp()
onResultReady(it)
viewModelScope.launch {
elementsToSend.forEach { signedDocument ->
try {
val issuerSignedEntries = signedDocument.issuerSignedEntries()
val result = withContext(Dispatchers.IO) { //<- Offload from UI thread
transferManager.addDocumentToResponse(
signedDocument.identityCredentialName,
signedDocument.documentType,
issuerSignedEntries,
responseGenerator,
authKey,
authKeyUnlockData,
)
}
)
} catch (e: CredentialInvalidatedException) {
logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.")
documentManager.deleteCredentialByName(signedDocument.identityCredentialName)
Toast.makeText(app.applicationContext, "Deleting invalid credential "
+ signedDocument.identityCredentialName,
Toast.LENGTH_SHORT).show()
} catch (e: NoSuchElementException) {
logWarning("No requestedDocument for " + signedDocument.documentType)
if (result !is AddDocumentToResponseResult.DocumentAdded) {
onResultReady(result)
return@forEach
}
transferManager.sendResponse(
responseGenerator.generate(),
PreferencesHelper.isConnectionAutoCloseEnabled()
)
transferManager.setResponseServed()
val documentsCount = elementsToSend.count()
documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount))
cleanUp()
onResultReady(result)
} catch (e: CredentialInvalidatedException) {
logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.")
documentManager.deleteCredentialByName(signedDocument.identityCredentialName)
Toast.makeText(
app.applicationContext, "Deleting invalid credential "
+ signedDocument.identityCredentialName,
Toast.LENGTH_SHORT
).show()
} catch (e: NoSuchElementException) {
logWarning("No requestedDocument for " + signedDocument.documentType)
}
}
}
}
Expand Down
Loading