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 #297 from admin-ch/feature/transfer-code-kpg-error
Browse files Browse the repository at this point in the history
Transfer Code KPG Error
  • Loading branch information
M-Wong authored Oct 21, 2021
2 parents c9913e5 + 30e3789 commit b18b7dc
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ 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.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.time.Instant
import kotlin.collections.set
Expand Down Expand Up @@ -257,11 +261,14 @@ class CertificatesViewModel(application: Application) : AndroidViewModel(applica
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) {
walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
didReplaceTransferCode = walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
conversionState = if (decodeState is DecodeState.SUCCESS) {
TransferCodeConversionState.CONVERTED(decodeState.certificateHolder)
Expand All @@ -272,7 +279,11 @@ class CertificatesViewModel(application: Application) : AndroidViewModel(applica
} 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class MainApplication : Application() {
CovidCertificateSdk.init(this, EnvironmentUtil.getSdkEnvironment())

migrateCertificatesToWalletData()
migrateTransferCodeValidity()

setupTransferWorker()
}
Expand All @@ -56,6 +57,19 @@ class MainApplication : Application() {
}
}

private fun migrateTransferCodeValidity() {
val walletStorage = WalletSecureStorage.getInstance(this)
if (!walletStorage.getMigratedTransferCodeValidity()) {
// Reading the wallet data once from the storage and writing it again immediately is enough to migrate the validity.
// The data class constructor defines the new fields with a default value, so it is automatically set when deserializing
val walletDataStorage = WalletDataSecureStorage.getInstance(this)
val walletDataItems = walletDataStorage.getWalletData()
walletDataStorage.updateWalletData(walletDataItems)

walletStorage.setMigratedTransferCodeValidity(true)
}
}

private fun setupTransferWorker() {
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package ch.admin.bag.covidcertificate.wallet.data

import android.content.Context
import androidx.core.content.edit
import ch.admin.bag.covidcertificate.sdk.android.utils.EncryptedSharedPreferencesUtil
import ch.admin.bag.covidcertificate.sdk.android.utils.SingletonHolder
import ch.admin.bag.covidcertificate.sdk.core.models.healthcert.CertificateHolder
Expand All @@ -20,6 +21,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import java.time.Instant
import java.util.concurrent.locks.ReentrantLock

class WalletDataSecureStorage private constructor(context: Context) {

Expand All @@ -36,6 +38,7 @@ class WalletDataSecureStorage private constructor(context: Context) {
}

private val prefs = EncryptedSharedPreferencesUtil.initializeSharedPreferences(context, SHARED_PREFERENCES_NAME)
private val reentrantLock = ReentrantLock()

fun saveWalletDataItem(dataItem: WalletDataItem) {
val walletData = getWalletData().toMutableList()
Expand Down Expand Up @@ -81,11 +84,14 @@ class WalletDataSecureStorage private constructor(context: Context) {
updateWalletData(walletData)
}

/**
* @return True if a transfer code was replaced with a certificate, false otherwise
*/
fun replaceTransferCodeWithCertificate(
transferCode: TransferCodeModel,
certificateQrCodeData: String,
pdfData: String? = null
) {
): Boolean {
val walletData = getWalletData().toMutableList()
val index =
walletData.indexOfFirst { it is WalletDataItem.TransferCodeWalletData && it.transferCode.code == transferCode.code }
Expand All @@ -95,7 +101,9 @@ class WalletDataSecureStorage private constructor(context: Context) {
walletData.add(index, WalletDataItem.CertificateWalletData(certificateQrCodeData, pdfData))
}
updateWalletData(walletData)
return true
}
return false
}

fun changeWalletDataItemPosition(oldPosition: Int, newPosition: Int) {
Expand All @@ -110,11 +118,17 @@ class WalletDataSecureStorage private constructor(context: Context) {
}

fun getWalletData(): List<WalletDataItem> {
val json = prefs.getString(KEY_WALLET_DATA_ITEMS, null)
if (json == null || json.isEmpty()) {
return emptyList()
reentrantLock.lock()

try {
val json = prefs.getString(KEY_WALLET_DATA_ITEMS, null)
if (json == null || json.isEmpty()) {
return emptyList()
}
return walletDataItemAdapter.fromJson(json) ?: emptyList()
} finally {
reentrantLock.unlock()
}
return walletDataItemAdapter.fromJson(json) ?: emptyList()
}

fun storeCertificateLight(fullCertificate: CertificateHolder, certificateLightData: String, certificateLightQrCode: String) {
Expand Down Expand Up @@ -169,9 +183,14 @@ class WalletDataSecureStorage private constructor(context: Context) {
}
}

private fun updateWalletData(walletData: List<WalletDataItem>) {
val json = walletDataItemAdapter.toJson(walletData)
prefs.edit().putString(KEY_WALLET_DATA_ITEMS, json).apply()
fun updateWalletData(walletData: List<WalletDataItem>) {
reentrantLock.lock()
try {
val json = walletDataItemAdapter.toJson(walletData)
prefs.edit { putString(KEY_WALLET_DATA_ITEMS, json) }
} finally {
reentrantLock.unlock()
}
}

private fun List<WalletDataItem>.containsCertificate(qrCodeData: String) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class WalletSecureStorage private constructor(context: Context) {
private const val PREFERENCES = "SecureStorage"
private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed"
private const val KEY_MIGRATED_CERTIFICATES_TO_WALLET_DATA = "KEY_MIGRATED_CERTIFICATES_TO_WALLET_DATA"
private const val KEY_MIGRATED_TRANSFER_CODE_VALIDITY = "KEY_MIGRATED_TRANSFER_CODE_VALIDITY"
private const val KEY_CERTIFICATE_LIGHT_UPDATEBOARDING_COMPLETED = "KEY_CERTIFICATE_LIGHT_UPDATEBOARDING_COMPLETED"
private const val KEY_TRANSFER_CODE_PUBLIC_KEY_PREFIX = "TRANSFER_CODE_PUBLIC_KEY_"
private const val KEY_TRANSFER_CODE_PRIVATE_KEY_PREFIX = "TRANSFER_CODE_PRIVATE_KEY_"
Expand All @@ -41,6 +42,12 @@ class WalletSecureStorage private constructor(context: Context) {
putBoolean(KEY_MIGRATED_CERTIFICATES_TO_WALLET_DATA, migrated)
}

fun getMigratedTransferCodeValidity() = prefs.getBoolean(KEY_MIGRATED_TRANSFER_CODE_VALIDITY, false)

fun setMigratedTransferCodeValidity(migrated: Boolean) = prefs.edit {
putBoolean(KEY_MIGRATED_TRANSFER_CODE_VALIDITY, migrated)
}

fun getCertificateLightUpdateboardingCompleted() = prefs.getBoolean(KEY_CERTIFICATE_LIGHT_UPDATEBOARDING_COMPLETED, false)

fun setCertificateLightUpdateboardingCompleted(completed: Boolean) = prefs.edit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import kotlinx.coroutines.withContext
import java.io.IOException
import java.security.KeyPair
import java.time.Instant
import java.time.temporal.ChronoUnit

class TransferCodeCreationViewModel(application: Application) : AndroidViewModel(application) {

Expand Down Expand Up @@ -73,7 +74,9 @@ class TransferCodeCreationViewModel(application: Application) : AndroidViewModel
when (registrationResponse) {
TransferCodeCreationResponse.SUCCESSFUL -> {
val now = Instant.now()
val transferCodeModel = TransferCodeModel(transferCode, now, now)
val expiresAt = now.plus(30, ChronoUnit.DAYS)
val failsAt = now.plus(72, ChronoUnit.HOURS)
val transferCodeModel = TransferCodeModel(transferCode, now, now, expiresAt, failsAt)
walletDataStorage.saveWalletDataItem(WalletDataItem.TransferCodeWalletData(transferCodeModel))
creationStateMutableLiveData.postValue(TransferCodeCreationState.SUCCESS(transferCodeModel))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ 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.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.IOException
import java.security.KeyPair

Expand Down Expand Up @@ -61,11 +65,13 @@ class TransferCodeViewModel(application: Application) : AndroidViewModel(applica
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) {
walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
didReplaceTransferCode = walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
val decodeState = CovidCertificateSdk.Wallet.decode(qrCodeData)
if (decodeState is DecodeState.SUCCESS) {
conversionStateMutableLiveData.postValue(TransferCodeConversionState.CONVERTED(decodeState.certificateHolder))
Expand All @@ -76,6 +82,11 @@ class TransferCodeViewModel(application: Application) : AndroidViewModel(applica
} 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)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@ import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt

/**
* The expiresAt and failsAt default values are set to 7d/72h due to the extended validity starting with the 2.7.0 release.
* Existing transfer codes should automatically be migrated with these default values, while newer codes should set the correct
* validity range based on the server configuration.
*/
@JsonClass(generateAdapter = true)
data class TransferCodeModel(
val code: String,
val creationTimestamp: Instant,
val lastUpdatedTimestamp: Instant,
val expiresAtTimestamp: Instant = creationTimestamp.plus(7, ChronoUnit.DAYS),
val failsAtTimestamp: Instant = expiresAtTimestamp.plus(72, ChronoUnit.HOURS),
): Serializable {

val expirationTimestamp = creationTimestamp.plus(30, ChronoUnit.DAYS)
val failureTimestamp = expirationTimestamp.plus(72, ChronoUnit.HOURS)

fun isExpired() = expirationTimestamp.isBefore(Instant.now())
fun isExpired() = expiresAtTimestamp.isBefore(Instant.now())

fun isFailed() = failureTimestamp.isBefore(Instant.now())
fun isFailed() = failsAtTimestamp.isBefore(Instant.now())

fun getDaysUntilExpiration(): Int {
val now = Instant.now().toEpochMilli()
val diff = expirationTimestamp.toEpochMilli() - now
val diff = expiresAtTimestamp.toEpochMilli() - now
return (diff.toDouble() / TimeUnit.DAYS.toMillis(1)).roundToInt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
package ch.admin.bag.covidcertificate.wallet.transfercode.worker

import android.content.Context
import androidx.work.*
import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import ch.admin.bag.covidcertificate.common.config.ConfigModel
import ch.admin.bag.covidcertificate.common.exception.TimeDeviationException
import ch.admin.bag.covidcertificate.wallet.BuildConfig
Expand Down Expand Up @@ -92,15 +97,21 @@ class TransferWorker(context: Context, workerParams: WorkerParameters) : Corouti
val decryptedCertificates = deliveryRepository.download(transferCode.code, keyPair)

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

decryptedCertificates.forEachIndexed { index, convertedCertificate ->
val qrCodeData = convertedCertificate.qrCodeData
val pdfData = convertedCertificate.pdfData
if (index == 0) {
walletDataStorage.replaceTransferCodeWithCertificate(transferCode, qrCodeData, pdfData)
didReplaceTransferCode = walletDataStorage.replaceTransferCodeWithCertificate(transferCode, 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) {
Expand Down

0 comments on commit b18b7dc

Please sign in to comment.