Skip to content

Commit f3e8998

Browse files
authored
Play Integrity: Add clientKey refresh timing (#3002)
1 parent 6241b53 commit f3e8998

File tree

2 files changed

+59
-21
lines changed

2 files changed

+59
-21
lines changed

vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ private const val DEVICE_INTEGRITY_SOFT_EXPIRATION_CHECK_PERIOD = 600L // 10 min
8888
private const val TEMPORARY_DEVICE_KEY_VALIDITY = 64800L // 18 hours
8989
private const val DEVICE_INTEGRITY_SOFT_EXPIRATION = 100800L // 28 hours
9090
private const val DEVICE_INTEGRITY_HARD_EXPIRATION = 432000L // 5 day
91-
91+
const val INTERMEDIATE_INTEGRITY_HARD_EXPIRATION = 86400L // 1 day
9292
private const val TAG = "IntegrityExtensions"
9393

94+
fun IntegrityRequestWrapper.getExpirationTime() = runCatching {
95+
val creationTimeStamp = deviceIntegrityWrapper?.creationTime ?: Timestamp(0, 0)
96+
val creationTime = (creationTimeStamp.seconds ?: 0) * 1000 + (creationTimeStamp.nanos ?: 0) / 1_000_000
97+
val currentTimeStamp = makeTimestamp(System.currentTimeMillis())
98+
val currentTime = (currentTimeStamp.seconds ?: 0) * 1000 + (currentTimeStamp.nanos ?: 0) / 1_000_000
99+
return@runCatching currentTime - creationTime
100+
}.getOrDefault(0)
101+
94102
private fun Context.getProtoFile(): File {
95103
val directory = File(filesDir, "finsky/shared")
96104
if (!directory.exists()) {
@@ -103,6 +111,12 @@ private fun Context.getProtoFile(): File {
103111
return file
104112
}
105113

114+
private fun getExpressFilePB(context: Context): ExpressFilePB {
115+
return runCatching { FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } }
116+
.onFailure { Log.w(TAG, "Failed to read express cache ", it) }
117+
.getOrDefault(ExpressFilePB())
118+
}
119+
106120
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo {
107121
return runCatching {
108122
if (Build.VERSION.SDK_INT >= 33) {
@@ -161,14 +175,14 @@ fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? {
161175
}
162176
}
163177

164-
suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO){
178+
suspend fun getIntegrityRequestWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, accountName: String) = withContext(Dispatchers.IO) {
165179
fun getUpdatedWebViewRequestMode(webViewRequestMode: Int): Int {
166180
return when (webViewRequestMode) {
167181
in 0..2 -> webViewRequestMode + 1
168182
else -> 1
169183
}
170184
}
171-
val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) }
185+
val expressFilePB = getExpressFilePB(context)
172186
expressFilePB.integrityRequestWrapper.filter { item ->
173187
TextUtils.equals(item.packageName, expressIntegritySession.packageName) && item.cloudProjectNumber == expressIntegritySession.cloudProjectNumber && getUpdatedWebViewRequestMode(
174188
expressIntegritySession.webViewRequestMode
@@ -237,7 +251,7 @@ fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): L
237251
suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) {
238252
Log.d(TAG, "Writing AAR to express cache")
239253
val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity
240-
val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) }
254+
val expressFilePB = getExpressFilePB(context)
241255

242256
val integrityResponseWrapper = IntegrityRequestWrapper.Builder().apply {
243257
accountName = intermediateIntegrity.accountName
@@ -285,7 +299,7 @@ suspend fun updateExpressSessionTime(context: Context, expressIntegritySession:
285299
expressIntegritySession.packageName
286300
}
287301

288-
val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) }
302+
val expressFilePB = getExpressFilePB(context)
289303

290304
val clientKey = expressFilePB.integrityTokenTimeMap ?: IntegrityTokenTimeMap()
291305
val timeMutableMap = clientKey.newBuilder().timeMap.toMutableMap()
@@ -307,21 +321,30 @@ suspend fun updateExpressSessionTime(context: Context, expressIntegritySession:
307321
}
308322

309323
suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.IO) {
310-
val expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) }
311-
324+
val expressFilePB = getExpressFilePB(context)
312325
val oldClientKey = expressFilePB.clientKey ?: ClientKey()
313-
var clientKey = ClientKey.Builder().apply {
314-
val currentTimeMillis = System.currentTimeMillis()
315-
generated = Timestamp.Builder().seconds(currentTimeMillis / 1000).nanos((Math.floorMod(currentTimeMillis, 1000L) * 1000000).toInt()).build()
326+
val generated = makeTimestamp(System.currentTimeMillis())
327+
328+
val oldGeneratedSec = oldClientKey.generated?.seconds ?: 0
329+
val newGeneratedSec = generated.seconds ?: 0
330+
331+
val useOld = oldClientKey.keySetHandle?.size != 0 && oldGeneratedSec >= newGeneratedSec - TEMPORARY_DEVICE_KEY_VALIDITY
332+
333+
val clientKey = if (useOld) {
334+
Log.d(TAG, "Using existing clientKey, not expired. oldGeneratedSec=$oldGeneratedSec newGeneratedSec=$newGeneratedSec")
335+
oldClientKey
336+
} else {
337+
Log.d(TAG, "Generating new clientKey. oldKeyValid=${oldClientKey.keySetHandle?.size != 0} expired=${oldGeneratedSec < newGeneratedSec - TEMPORARY_DEVICE_KEY_VALIDITY}")
316338
val keySetHandle = KeysetHandle.generateNew(AesGcmKeyManager.aes128GcmTemplate())
317-
val outputStream = ByteArrayOutputStream()
318-
CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(outputStream))
319-
this.keySetHandle = ByteBuffer.wrap(outputStream.toByteArray()).toByteString()
320-
}.build()
321-
if (oldClientKey.keySetHandle?.size != 0) {
322-
if (oldClientKey.generated?.seconds != null && clientKey.generated?.seconds != null && oldClientKey.generated.seconds < clientKey.generated?.seconds!!.minus(TEMPORARY_DEVICE_KEY_VALIDITY)) {
323-
clientKey = oldClientKey
339+
val keyBytes = ByteArrayOutputStream().use { output ->
340+
CleartextKeysetHandle.write(keySetHandle, BinaryKeysetWriter.withOutputStream(output))
341+
output.toByteArray()
324342
}
343+
Log.d(TAG, "New clientKey generated at timestamp: ${generated.seconds}")
344+
ClientKey.Builder()
345+
.generated(generated)
346+
.keySetHandle(ByteBuffer.wrap(keyBytes).toByteString())
347+
.build()
325348
}
326349

327350
val newExpressFilePB = expressFilePB.newBuilder().clientKey(clientKey).build()
@@ -330,7 +353,7 @@ suspend fun updateExpressClientKey(context: Context) = withContext(Dispatchers.I
330353
}
331354

332355
suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySession: ExpressIntegritySession, authToken: String, clientKey: ClientKey) = withContext(Dispatchers.IO) {
333-
var expressFilePB = FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) }
356+
var expressFilePB = getExpressFilePB(context)
334357

335358
val createTimeSeconds = expressFilePB.tokenWrapper?.deviceIntegrityWrapper?.creationTime?.seconds ?: 0
336359
val lastManualSoftRefreshTime = expressFilePB.tokenWrapper?.lastManualSoftRefreshTime?.seconds ?: 0
@@ -386,6 +409,7 @@ private suspend fun regenerateToken(
386409
this.deviceIntegrityToken = deviceIntegrityToken ?: ByteString.EMPTY
387410
this.creationTime = makeTimestamp(System.currentTimeMillis())
388411
}.build()
412+
this.lastManualSoftRefreshTime = makeTimestamp(System.currentTimeMillis())
389413
}.build()
390414
} catch (e: Exception) {
391415
Log.d(TAG, "regenerateToken: error ", e)

vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.google.android.finsky.ClientKey
2626
import com.google.android.finsky.ClientKeyExtend
2727
import com.google.android.finsky.DeviceIntegrityWrapper
2828
import com.google.android.finsky.ExpressIntegrityResponse
29+
import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION
2930
import com.google.android.finsky.IntermediateIntegrityRequest
3031
import com.google.android.finsky.IntermediateIntegritySession
3132
import com.google.android.finsky.KEY_CLOUD_PROJECT
@@ -45,6 +46,7 @@ import com.google.android.finsky.RequestMode
4546
import com.google.android.finsky.getPlayCoreVersion
4647
import com.google.android.finsky.encodeBase64
4748
import com.google.android.finsky.getAuthToken
49+
import com.google.android.finsky.getExpirationTime
4850
import com.google.android.finsky.getIntegrityRequestWrapper
4951
import com.google.android.finsky.getPackageInfoCompat
5052
import com.google.android.finsky.model.IntegrityErrorCode
@@ -195,19 +197,22 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
195197
}
196198
}.getOrDefault(RESULT_UN_AUTH)
197199

200+
val refreshClientKey = clientKey.newBuilder()
201+
.generated(makeTimestamp(System.currentTimeMillis()))
202+
.build()
198203
val intermediateIntegrityResponseData = IntermediateIntegrityResponseData(
199204
intermediateIntegrity = IntermediateIntegrity(
200205
expressIntegritySession.packageName,
201206
expressIntegritySession.cloudProjectNumber,
202207
defaultAccountName,
203-
clientKey,
208+
refreshClientKey,
204209
intermediateIntegrityResponse.intermediateToken,
205210
intermediateIntegrityResponse.serverGenerated,
206211
expressIntegritySession.webViewRequestMode,
207212
0
208213
),
209214
callerKeyMd5 = Base64.encodeToString(
210-
clientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
215+
refreshClientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
211216
),
212217
appVersionCode = packageInformation.versionCode,
213218
deviceIntegrityResponse = deviceIntegrityResponse,
@@ -272,8 +277,17 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
272277
return@launchWhenCreated
273278
}
274279

280+
val expirationTime = integrityRequestWrapper.getExpirationTime()
281+
282+
if (expirationTime > INTERMEDIATE_INTEGRITY_HARD_EXPIRATION * 1000) {
283+
Log.w(TAG, "Intermediate integrity hard expiration reached.")
284+
callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID))
285+
return@launchWhenCreated
286+
}
287+
Log.d(TAG, "Intermediate integrity token generated time $expirationTime.")
288+
275289
val integritySession = IntermediateIntegritySession.Builder().creationTime(makeTimestamp(System.currentTimeMillis())).requestHash(expressIntegritySession.requestHash)
276-
.sessionId(Random.nextBytes(8).toByteString()).timestampMillis(0).build()
290+
.sessionId(Random.nextBytes(8).toByteString()).timestampMillis(expirationTime.toInt()).build()
277291

278292
val expressIntegrityResponse = ExpressIntegrityResponse.Builder().apply {
279293
this.deviceIntegrityToken = integrityRequestWrapper.deviceIntegrityWrapper?.deviceIntegrityToken

0 commit comments

Comments
 (0)