From 6cc7f5be6fe06126925efc44977dfb8d71a64bf2 Mon Sep 17 00:00:00 2001 From: Davinci9196 Date: Wed, 6 Aug 2025 11:12:51 +0800 Subject: [PATCH] PI: Add clientKey refresh timing --- .../android/finsky/IntegrityExtensions.kt | 60 +++++++++++++------ .../ExpressIntegrityService.kt | 20 ++++++- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt index df9c796799..2598ef2bae 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt @@ -88,9 +88,17 @@ private const val DEVICE_INTEGRITY_SOFT_EXPIRATION_CHECK_PERIOD = 600L // 10 min private const val TEMPORARY_DEVICE_KEY_VALIDITY = 64800L // 18 hours private const val DEVICE_INTEGRITY_SOFT_EXPIRATION = 100800L // 28 hours private const val DEVICE_INTEGRITY_HARD_EXPIRATION = 432000L // 5 day - +const val INTERMEDIATE_INTEGRITY_HARD_EXPIRATION = 86400L // 1 day private const val TAG = "IntegrityExtensions" +fun IntegrityRequestWrapper.getExpirationTime() = runCatching { + val creationTimeStamp = deviceIntegrityWrapper?.creationTime ?: Timestamp(0, 0) + val creationTime = (creationTimeStamp.seconds ?: 0) * 1000 + (creationTimeStamp.nanos ?: 0) / 1_000_000 + val currentTimeStamp = makeTimestamp(System.currentTimeMillis()) + val currentTime = (currentTimeStamp.seconds ?: 0) * 1000 + (currentTimeStamp.nanos ?: 0) / 1_000_000 + return@runCatching currentTime - creationTime +}.getOrDefault(0) + private fun Context.getProtoFile(): File { val directory = File(filesDir, "finsky/shared") if (!directory.exists()) { @@ -103,6 +111,12 @@ private fun Context.getProtoFile(): File { return file } +private fun getExpressFilePB(context: Context): ExpressFilePB { + return runCatching { FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } } + .onFailure { Log.w(TAG, "Failed to read express cache ", it) } + .getOrDefault(ExpressFilePB()) +} + fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo { return runCatching { if (Build.VERSION.SDK_INT >= 33) { @@ -161,14 +175,14 @@ fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? { } } -suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO){ +suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO) { fun getUpdatedWebViewRequestMode(webViewRequestMode: Int): Int { return when (webViewRequestMode) { in 0..2 -> webViewRequestMode + 1 else -> 1 } } - val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + val expressFilePB = getExpressFilePB(context) expressFilePB.integrityRequestWrapper.filter { item -> TextUtils.equals(item.packageName, expressIntegritySession.packageName) && item.cloudProjectNumber == expressIntegritySession.cloudProjectNumber && getUpdatedWebViewRequestMode( expressIntegritySession.webViewRequestMode @@ -237,7 +251,7 @@ fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): L suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) { Log.d(TAG, "Writing AAR to express cache") val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity - val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + val expressFilePB = getExpressFilePB(context) val integrityResponseWrapper = IntegrityRequestWrapper.Builder().apply { accountName = intermediateIntegrity.accountName @@ -285,7 +299,7 @@ suspend fun updateExpressSessionTime(context: Context, expressIntegritySession: expressIntegritySession.packageName } - val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + val expressFilePB = getExpressFilePB(context) val clientKey = expressFilePB.integrityTokenTimeMap ?: IntegrityTokenTimeMap() val timeMutableMap = clientKey.newBuilder().timeMap.toMutableMap() @@ -307,21 +321,30 @@ suspend fun updateExpressSessionTime(context: Context, expressIntegritySession: } suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.IO) { - val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } - + val expressFilePB = getExpressFilePB(context) val oldClientKey = expressFilePB.clientKey ?: ClientKey() - var clientKey = ClientKey.Builder().apply { - val currentTimeMillis = System.currentTimeMillis() - generated = Timestamp.Builder().seconds(currentTimeMillis / 1000).nanos((Math.floorMod(currentTimeMillis, 1000L) * 1000000).toInt()).build() + val generated = makeTimestamp(System.currentTimeMillis()) + + val oldGeneratedSec = oldClientKey.generated?.seconds ?: 0 + val newGeneratedSec = generated.seconds ?: 0 + + val useOld = oldClientKey.keySetHandle?.size != 0 && oldGeneratedSec >= newGeneratedSec - TEMPORARY_DEVICE_KEY_VALIDITY + + val clientKey = if (useOld) { + Log.d(TAG, "Using existing clientKey, not expired. oldGeneratedSec=$oldGeneratedSec newGeneratedSec=$newGeneratedSec") + oldClientKey + } else { + Log.d(TAG, "Generating new clientKey. oldKeyValid=${oldClientKey.keySetHandle?.size != 0} expired=${oldGeneratedSec < newGeneratedSec - TEMPORARY_DEVICE_KEY_VALIDITY}") val keySetHandle = KeysetHandle.generateNew(AesGcmKeyManager.aes128GcmTemplate()) - val outputStream = ByteArrayOutputStream() - CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(outputStream)) - this.keySetHandle = ByteBuffer.wrap(outputStream.toByteArray()).toByteString() - }.build() - if (oldClientKey.keySetHandle?.size != 0) { - if (oldClientKey.generated?.seconds != null && clientKey.generated?.seconds != null && oldClientKey.generated.seconds < clientKey.generated?.seconds!!.minus(TEMPORARY_DEVICE_KEY_VALIDITY)) { - clientKey = oldClientKey + val keyBytes = ByteArrayOutputStream().use { output -> + CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(output)) + output.toByteArray() } + Log.d(TAG, "New clientKey generated at timestamp: ${generated.seconds}") + ClientKey.Builder() + .generated(generated) + .keySetHandle(ByteBuffer.wrap(keyBytes).toByteString()) + .build() } val newExpressFilePB = expressFilePB.newBuilder().clientKey(clientKey).build() @@ -330,7 +353,7 @@ suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.I } suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, authToken: String, clientKey: ClientKey) = withContext(Dispatchers.IO) { - var expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } + var expressFilePB = getExpressFilePB(context) val createTimeSeconds = expressFilePB.tokenWrapper?.deviceIntegrityWrapper?.creationTime?.seconds ?: 0 val lastManualSoftRefreshTime = expressFilePB.tokenWrapper?.lastManualSoftRefreshTime?.seconds ?: 0 @@ -386,6 +409,7 @@ private suspend fun regenerateToken( this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY this.creationTime = makeTimestamp(System.currentTimeMillis()) }.build() + this.lastManualSoftRefreshTime = makeTimestamp(System.currentTimeMillis()) }.build() } catch (e: Exception) { Log.d(TAG, "regenerateToken: error ", e) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt index 386dfe5976..ec6796ab91 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt @@ -26,6 +26,7 @@ import com.google.android.finsky.ClientKey import com.google.android.finsky.ClientKeyExtend import com.google.android.finsky.DeviceIntegrityWrapper import com.google.android.finsky.ExpressIntegrityResponse +import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION import com.google.android.finsky.IntermediateIntegrityRequest import com.google.android.finsky.IntermediateIntegritySession import com.google.android.finsky.KEY_CLOUD_PROJECT @@ -45,6 +46,7 @@ import com.google.android.finsky.RequestMode import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.encodeBase64 import com.google.android.finsky.getAuthToken +import com.google.android.finsky.getExpirationTime import com.google.android.finsky.getIntegrityRequestWrapper import com.google.android.finsky.getPackageInfoCompat import com.google.android.finsky.model.IntegrityErrorCode @@ -195,19 +197,22 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override } }.getOrDefault(RESULT_UN_AUTH) + val refreshClientKey = clientKey.newBuilder() + .generated(makeTimestamp(System.currentTimeMillis())) + .build() val intermediateIntegrityResponseData = IntermediateIntegrityResponseData( intermediateIntegrity = IntermediateIntegrity( expressIntegritySession.packageName, expressIntegritySession.cloudProjectNumber, defaultAccountName, - clientKey, + refreshClientKey, intermediateIntegrityResponse.intermediateToken, intermediateIntegrityResponse.serverGenerated, expressIntegritySession.webViewRequestMode, 0 ), callerKeyMd5 = Base64.encodeToString( - clientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + refreshClientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING ), appVersionCode = packageInformation.versionCode, deviceIntegrityResponse = deviceIntegrityResponse, @@ -272,8 +277,17 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override return@launchWhenCreated } + val expirationTime = integrityRequestWrapper.getExpirationTime() + + if (expirationTime > INTERMEDIATE_INTEGRITY_HARD_EXPIRATION * 1000) { + Log.w(TAG, "Intermediate integrity hard expiration reached.") + callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID)) + return@launchWhenCreated + } + Log.d(TAG, "Intermediate integrity token generated time $expirationTime.") + val integritySession = IntermediateIntegritySession.Builder().creationTime(makeTimestamp(System.currentTimeMillis())).requestHash(expressIntegritySession.requestHash) - .sessionId(Random.nextBytes(8).toByteString()).timestampMillis(0).build() + .sessionId(Random.nextBytes(8).toByteString()).timestampMillis(expirationTime.toInt()).build() val expressIntegrityResponse = ExpressIntegrityResponse.Builder().apply { this.deviceIntegrityToken = integrityRequestWrapper.deviceIntegrityWrapper?.deviceIntegrityToken