Skip to content
This repository has been archived by the owner on Sep 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #300 from admin-ch/feature/lock-transfer-code-down…
Browse files Browse the repository at this point in the history
…load

Feature/lock transfer code download
  • Loading branch information
M-Wong authored Oct 21, 2021
2 parents b18b7dc + 508eb22 commit 83523af
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ import ch.admin.bag.covidcertificate.wallet.transfercode.model.TransferCodeConve
import ch.admin.bag.covidcertificate.wallet.transfercode.model.TransferCodeModel
import ch.admin.bag.covidcertificate.wallet.transfercode.net.DeliveryRepository
import ch.admin.bag.covidcertificate.wallet.transfercode.net.DeliverySpec
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.sync.withLock
import java.io.IOException
import java.time.Instant
import kotlin.collections.set
Expand Down Expand Up @@ -253,66 +250,79 @@ class CertificatesViewModel(application: Application) : AndroidViewModel(applica

private fun convertTransferCode(transferCode: TransferCodeModel) {
viewModelScope.launch(Dispatchers.IO) {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())

var conversionState: TransferCodeConversionState = TransferCodeConversionState.LOADING
if (keyPair != null) {
try {
val decryptedCertificates = deliveryRepository.download(transferCode.code, keyPair)

if (decryptedCertificates.isNotEmpty()) {
var didReplaceTransferCode = false

decryptedCertificates.forEachIndexed { index, convertedCertificate ->
val qrCodeData = convertedCertificate.qrCodeData
val pdfData = convertedCertificate.pdfData

if (index == 0) {
didReplaceTransferCode = walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
conversionState = if (decodeState is DecodeState.SUCCESS) {
TransferCodeConversionState.CONVERTED(decodeState.certificateHolder)
TransferCodeCrypto.getMutex(transferCode.code).withLock {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())

var conversionState: TransferCodeConversionState = TransferCodeConversionState.LOADING
if (keyPair != null) {
try {
val decryptedCertificates = deliveryRepository.download(transferCode.code, keyPair)

if (decryptedCertificates.isNotEmpty()) {
var didReplaceTransferCode = false

decryptedCertificates.forEachIndexed { index, convertedCertificate ->
val qrCodeData = convertedCertificate.qrCodeData
val pdfData = convertedCertificate.pdfData

if (index == 0) {
didReplaceTransferCode =
walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
conversionState = if (decodeState is DecodeState.SUCCESS) {
MainApplication.getTransferCodeConversionMapping(getApplication())
?.put(transferCode.code, decodeState.certificateHolder)
TransferCodeConversionState.CONVERTED(decodeState.certificateHolder)
} else {
// The certificate returned from the server could not be decoded
TransferCodeConversionState.NOT_CONVERTED
}
} else {
// The certificate returned from the server could not be decoded
TransferCodeConversionState.NOT_CONVERTED
walletDataStorage.saveWalletDataItem(WalletDataItem.CertificateWalletData(qrCodeData, pdfData))
}
} else {
walletDataStorage.saveWalletDataItem(WalletDataItem.CertificateWalletData(qrCodeData, pdfData))
}
}

// Delete the transfer code on the backend and the key pair only if the certificate was stored (either by the above replace method or from another thread)
val didStoreCertificate = walletDataStorage.containsCertificate(decryptedCertificates.first().qrCodeData)
if (didReplaceTransferCode || didStoreCertificate) {
try {
deliveryRepository.complete(transferCode.code, keyPair)
} catch (e: IOException) {
// This request is best-effort, if it fails, ignore it and let the backend delete the transfer code and certificate
// automatically after it expires
// Delete the transfer code on the backend and the key pair only if the certificate was stored (either by the above replace method or from another thread)
val didStoreCertificate =
walletDataStorage.containsCertificate(decryptedCertificates.first().qrCodeData)
if (didReplaceTransferCode || didStoreCertificate) {
try {
deliveryRepository.complete(transferCode.code, keyPair)
} catch (e: IOException) {
// This request is best-effort, if it fails, ignore it and let the backend delete the transfer code and certificate
// automatically after it expires
}
TransferCodeCrypto.deleteKeyEntry(transferCode.code, getApplication())
}
TransferCodeCrypto.deleteKeyEntry(transferCode.code, getApplication())
} else {
conversionState = TransferCodeConversionState.NOT_CONVERTED
}
} catch (e: TimeDeviationException) {
conversionState = TransferCodeConversionState.ERROR(StateError(DeliveryRepository.ERROR_CODE_INVALID_TIME))
} catch (e: IOException) {
conversionState = if (NetworkUtil.isNetworkAvailable(connectivityManager)) {
TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_NETWORK_FAILURE))
} else {
TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_OFFLINE))
}
} else {
conversionState = TransferCodeConversionState.NOT_CONVERTED
}
} catch (e: TimeDeviationException) {
conversionState = TransferCodeConversionState.ERROR(StateError(DeliveryRepository.ERROR_CODE_INVALID_TIME))
} catch (e: IOException) {
conversionState = if (NetworkUtil.isNetworkAvailable(connectivityManager)) {
TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_NETWORK_FAILURE))
} else {
val alreadyLoadedCertificate =
MainApplication.getTransferCodeConversionMapping(getApplication())?.get(transferCode.code)
if (alreadyLoadedCertificate != null) {
conversionState = TransferCodeConversionState.CONVERTED(alreadyLoadedCertificate)
} else {
TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_OFFLINE))
conversionState =
TransferCodeConversionState.ERROR(StateError(TransferCodeErrorCodes.INAPP_DELIVERY_KEYPAIR_GENERATION_FAILED))
}
}
} else {
conversionState = TransferCodeConversionState.ERROR(StateError(TransferCodeErrorCodes.INAPP_DELIVERY_KEYPAIR_GENERATION_FAILED))
}

val updatedStatefulWalletItems = updateConversionStateForTransferCode(transferCode, conversionState)
val updatedStatefulWalletItems = updateConversionStateForTransferCode(transferCode, conversionState)

// Set the livedata value on the main dispatcher to prevent multiple posts overriding each other
withContext(Dispatchers.Main.immediate) {
statefulWalletItemsMutableLiveData.value = updatedStatefulWalletItems
// Set the livedata value on the main dispatcher to prevent multiple posts overriding each other
withContext(Dispatchers.Main.immediate) {
statefulWalletItemsMutableLiveData.value = updatedStatefulWalletItems
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ch.admin.bag.covidcertificate.wallet

import android.app.Application
import android.content.Context
import android.os.Build
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
Expand All @@ -12,6 +13,7 @@ import ch.admin.bag.covidcertificate.common.util.EnvironmentUtil
import ch.admin.bag.covidcertificate.sdk.android.CovidCertificateSdk
import ch.admin.bag.covidcertificate.sdk.android.data.Config
import ch.admin.bag.covidcertificate.sdk.android.net.interceptor.UserAgentInterceptor
import ch.admin.bag.covidcertificate.sdk.core.models.healthcert.CertificateHolder
import ch.admin.bag.covidcertificate.wallet.data.CertificateStorage
import ch.admin.bag.covidcertificate.wallet.data.WalletDataItem
import ch.admin.bag.covidcertificate.wallet.data.WalletDataSecureStorage
Expand All @@ -21,6 +23,21 @@ import ch.admin.bag.covidcertificate.wallet.util.NotificationUtil

class MainApplication : Application() {

companion object {

fun getTransferCodeConversionMapping(context: Context): HashMap<String, CertificateHolder>? {
val applicationContext = context.applicationContext
if (applicationContext is MainApplication) {
return applicationContext.getTransferCodeConversionMappingInternal()
} else {
return null
}
}

}

val transferCodeConversionMapping = HashMap<String, CertificateHolder>()

override fun onCreate() {
super.onCreate()

Expand Down Expand Up @@ -90,4 +107,7 @@ class MainApplication : Application() {
})
NotificationUtil.createTransferNotificationChannel(this)
}

private fun getTransferCodeConversionMappingInternal() = transferCodeConversionMapping

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,16 @@ import ch.admin.bag.covidcertificate.sdk.core.data.ErrorCodes
import ch.admin.bag.covidcertificate.sdk.core.models.state.DecodeState
import ch.admin.bag.covidcertificate.sdk.core.models.state.StateError
import ch.admin.bag.covidcertificate.wallet.BuildConfig
import ch.admin.bag.covidcertificate.wallet.MainApplication
import ch.admin.bag.covidcertificate.wallet.data.WalletDataItem
import ch.admin.bag.covidcertificate.wallet.data.WalletDataSecureStorage
import ch.admin.bag.covidcertificate.wallet.transfercode.logic.TransferCodeCrypto
import ch.admin.bag.covidcertificate.wallet.transfercode.model.TransferCodeConversionState
import ch.admin.bag.covidcertificate.wallet.transfercode.model.TransferCodeModel
import ch.admin.bag.covidcertificate.wallet.transfercode.net.DeliveryRepository
import ch.admin.bag.covidcertificate.wallet.transfercode.net.DeliverySpec
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
import java.io.IOException
import java.security.KeyPair

Expand All @@ -56,58 +54,70 @@ class TransferCodeViewModel(application: Application) : AndroidViewModel(applica

conversionStateMutableLiveData.value = TransferCodeConversionState.LOADING
downloadJob = viewModelScope.launch(Dispatchers.IO) {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())

if (keyPair != null) {
try {
if (delayInMillis > 0) delay(delayInMillis)

val decryptedCertificates = deliveryRepository.download(transferCode.code, keyPair)

if (decryptedCertificates.isNotEmpty()) {
var didReplaceTransferCode = false

decryptedCertificates.forEachIndexed { index, convertedCertificate ->
val qrCodeData = convertedCertificate.qrCodeData
val pdfData = convertedCertificate.pdfData
if (index == 0) {
didReplaceTransferCode = walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
if (decodeState is DecodeState.SUCCESS) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.CONVERTED(decodeState.certificateHolder))
TransferCodeCrypto.getMutex(transferCode.code).withLock {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())

if (keyPair != null) {
try {
if (delayInMillis > 0) delay(delayInMillis)

val decryptedCertificates = deliveryRepository.download(transferCode.code, keyPair)

if (decryptedCertificates.isNotEmpty()) {
var didReplaceTransferCode = false

decryptedCertificates.forEachIndexed { index, convertedCertificate ->
val qrCodeData = convertedCertificate.qrCodeData
val pdfData = convertedCertificate.pdfData
if (index == 0) {
didReplaceTransferCode =
walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
if (decodeState is DecodeState.SUCCESS) {
MainApplication.getTransferCodeConversionMapping(getApplication())
?.put(transferCode.code, decodeState.certificateHolder)
conversionStateMutableLiveData.postValue(TransferCodeConversionState.CONVERTED(decodeState.certificateHolder))
} else {
// The certificate returned from the server could not be decoded
conversionStateMutableLiveData.postValue(TransferCodeConversionState.NOT_CONVERTED)
}
} else {
// The certificate returned from the server could not be decoded
conversionStateMutableLiveData.postValue(TransferCodeConversionState.NOT_CONVERTED)
walletDataStorage.saveWalletDataItem(WalletDataItem.CertificateWalletData(qrCodeData, pdfData))
}
} else {
walletDataStorage.saveWalletDataItem(WalletDataItem.CertificateWalletData(qrCodeData, pdfData))
}
}

// Delete the transfer code on the backend and the key pair only if the certificate was stored (either by the above replace method or from another thread)
val didStoreCertificate = walletDataStorage.containsCertificate(decryptedCertificates.first().qrCodeData)
if (didReplaceTransferCode || didStoreCertificate) {
deleteTransferCodeOnServer(transferCode, keyPair)
// Delete the transfer code on the backend and the key pair only if the certificate was stored (either by the above replace method or from another thread)
val didStoreCertificate =
walletDataStorage.containsCertificate(decryptedCertificates.first().qrCodeData)
if (didReplaceTransferCode || didStoreCertificate) {
deleteTransferCodeOnServer(transferCode, keyPair)
}
} else {
// The server returned no certificate
conversionStateMutableLiveData.postValue(TransferCodeConversionState.NOT_CONVERTED)
}
} catch (e: TimeDeviationException) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(DeliveryRepository.ERROR_CODE_INVALID_TIME)))
} catch (e: IOException) {
// A request failed, check if the device has network connectivity or not
if (NetworkUtil.isNetworkAvailable(connectivityManager)) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_NETWORK_FAILURE)))
} else {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_OFFLINE)))
}
} else {
// The server returned no certificate
conversionStateMutableLiveData.postValue(TransferCodeConversionState.NOT_CONVERTED)
}
} catch (e: TimeDeviationException) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(DeliveryRepository.ERROR_CODE_INVALID_TIME)))
} catch (e: IOException) {
// A request failed, check if the device has network connectivity or not
if (NetworkUtil.isNetworkAvailable(connectivityManager)) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_NETWORK_FAILURE)))
} else {
val alreadyLoadedCertificate =
MainApplication.getTransferCodeConversionMapping(getApplication())?.get(transferCode.code)
if (alreadyLoadedCertificate != null) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.CONVERTED(alreadyLoadedCertificate))
} else {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(ErrorCodes.GENERAL_OFFLINE)))
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(TransferCodeErrorCodes.INAPP_DELIVERY_KEYPAIR_GENERATION_FAILED)))
}
}
} else {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.ERROR(StateError(TransferCodeErrorCodes.INAPP_DELIVERY_KEYPAIR_GENERATION_FAILED)))
}

downloadJob = null
downloadJob = null
}
}
}

Expand All @@ -116,9 +126,11 @@ class TransferCodeViewModel(application: Application) : AndroidViewModel(applica
// In that case, the viewModelScope wouldn't make sense because it is cleared when the fragment is popped.
// For this kind of fire-and-forget coroutine, the GlobalScope is therefore fine.
GlobalScope.launch(Dispatchers.IO) {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())
if (keyPair != null) {
deleteTransferCodeOnServer(transferCode, keyPair)
TransferCodeCrypto.getMutex(transferCode.code).withLock {
val keyPair = TransferCodeCrypto.loadKeyPair(transferCode.code, getApplication())
if (keyPair != null) {
deleteTransferCodeOnServer(transferCode, keyPair)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.security.keystore.KeyProperties
import ch.admin.bag.covidcertificate.sdk.core.extensions.fromBase64
import ch.admin.bag.covidcertificate.sdk.core.extensions.toBase64
import ch.admin.bag.covidcertificate.wallet.data.WalletSecureStorage
import kotlinx.coroutines.sync.Mutex
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.KeyFactory
import java.security.KeyPair
Expand Down Expand Up @@ -50,6 +51,19 @@ object TransferCodeCrypto {
}
}

private val mutexMap = HashMap<String, Mutex>()

@Synchronized fun getMutex(alias: String): Mutex {
var mutex = mutexMap.get(alias)
if(mutex == null){
mutex = Mutex()
mutexMap.put(alias, mutex)
return mutex
}else{
return mutex
}
}

fun createKeyPair(keyAlias: String, context: Context): KeyPair? {
val keyPurpose =
KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
Expand Down
Loading

0 comments on commit 83523af

Please sign in to comment.