Skip to content
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 @@ -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()) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down