Skip to content

Commit 445cd54

Browse files
@W-21933885: [MSDK Android] App Attestation Implementation (Extract To New App Attestation Client Classes)
1 parent 5be3d80 commit 445cd54

6 files changed

Lines changed: 294 additions & 135 deletions

File tree

libs/SalesforceSDK/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation("com.google.android.material:material:1.13.0") // remove this when all xml is gone
2929
implementation("androidx.appcompat:appcompat:1.7.1")
3030
implementation("androidx.biometric:biometric:1.2.0-alpha05")
31+
implementation("androidx.datastore:datastore:1.1.1") // Update requires Kotlin 2.
3132
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
3233
implementation("androidx.core:core-ktx:1.16.0") // Update requires API 36 compileSdk
3334
implementation("androidx.activity:activity-ktx:$androidXActivityVersion")

libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt

Lines changed: 10 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import android.provider.Settings.Secure.ANDROID_ID
5252
import android.provider.Settings.Secure.getString
5353
import android.text.TextUtils.isEmpty
5454
import android.text.TextUtils.join
55-
import android.util.Log
5655
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
5756
import android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
5857
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
@@ -72,10 +71,6 @@ import androidx.window.core.layout.WindowHeightSizeClass
7271
import androidx.window.core.layout.WindowSizeClass
7372
import androidx.window.core.layout.WindowWidthSizeClass
7473
import androidx.window.layout.WindowMetricsCalculator
75-
import com.google.android.play.core.integrity.IntegrityManagerFactory.createStandard
76-
import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
77-
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
78-
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
7974
import com.salesforce.androidsdk.BuildConfig.DEBUG
8075
import com.salesforce.androidsdk.R.string.account_type
8176
import com.salesforce.androidsdk.R.string.sf__dev_support_title
@@ -94,6 +89,7 @@ import com.salesforce.androidsdk.app.Features.FEATURE_BROWSER_LOGIN
9489
import com.salesforce.androidsdk.app.Features.FEATURE_NATIVE_LOGIN
9590
import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.DARK
9691
import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.SYSTEM_DEFAULT
92+
import com.salesforce.androidsdk.auth.AppAttestationClient
9793
import com.salesforce.androidsdk.auth.AuthenticatorService.KEY_INSTANCE_URL
9894
import com.salesforce.androidsdk.auth.HttpAccess
9995
import com.salesforce.androidsdk.auth.HttpAccess.DEFAULT
@@ -128,7 +124,6 @@ import com.salesforce.androidsdk.push.PushNotificationInterface
128124
import com.salesforce.androidsdk.push.PushService
129125
import com.salesforce.androidsdk.push.PushService.Companion.pushNotificationsRegistrationType
130126
import com.salesforce.androidsdk.push.PushService.PushNotificationReRegistrationType.ReRegistrationOnAppForeground
131-
import com.salesforce.androidsdk.rest.AppAttestationChallengeApiClient
132127
import com.salesforce.androidsdk.rest.ClientManager
133128
import com.salesforce.androidsdk.rest.NotificationsActionsResponseBody
134129
import com.salesforce.androidsdk.rest.NotificationsApiClient
@@ -154,17 +149,10 @@ import kotlinx.coroutines.CoroutineScope
154149
import kotlinx.coroutines.Dispatchers.Default
155150
import kotlinx.coroutines.Dispatchers.Main
156151
import kotlinx.coroutines.launch
157-
import kotlinx.coroutines.runBlocking
158-
import kotlinx.coroutines.tasks.await
159152
import kotlinx.coroutines.withTimeoutOrNull
160-
import kotlinx.serialization.Serializable
161-
import kotlinx.serialization.json.Json
162153
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
163154
import java.lang.String.CASE_INSENSITIVE_ORDER
164155
import java.net.URI
165-
import java.nio.charset.StandardCharsets.UTF_8
166-
import java.security.MessageDigest
167-
import java.util.Base64
168156
import java.util.Locale.US
169157
import java.util.SortedSet
170158
import java.util.UUID.randomUUID
@@ -696,123 +684,17 @@ open class SalesforceSDKManager protected constructor(
696684
)
697685
}
698686

699-
/** The Google Play Integrity API Token Provider */
700-
private var integrityTokenProvider: StandardIntegrityTokenProvider? = null
701-
702687
/**
703-
* A simple proof-of-concept to prepare for authorization and authorization
704-
* token refresh with the Salesforce Mobile App Attestation Challenge API
705-
* and Google Play Integrity API enabled.
706-
*
707-
* TODO: This will need to be made production-ready in the future. ECJ20260312
708-
// TODO: Discuss a suitable scope for this as attaching it to this singleton may further legacy patterns. ECJ20260312
688+
* Creates a client for use with the Salesforce App Attestation External
689+
* Client App (ECA) Plugin
690+
* @return The app authentication attestation client
709691
*/
710-
fun testGooglePlayIntegrityApiPreparation() {
711-
CoroutineScope(Default).launch {
712-
// The app's corresponding Cloud Project Number.
713-
// TODO: Determine where this value would be provided to or by Salesforce Mobile SDK. ECJ20260311
714-
val cloudProjectNumber = -1L // TODO: Google Cloud Project Number. ECJ20260311
715-
716-
// TODO: For production, determine where Salesforce Mobile SDK should encapsulate the logic of preparing the Google Play Integrity Manager and Token Provider. That can likely be a single method which encapsulates storing the token for later use in authorization plus refreshing the Token Provider when needed. ECJ20260311
717-
718-
// Create the Google Play Integrity Manager and Token Provider.
719-
val integrityManager = createStandard(this@SalesforceSDKManager.appContext)
720-
721-
// Prepare the Google Play Integrity token. Calling this prior to requesting the Integrity Token reduces the latency of the request.
722-
integrityManager.prepareIntegrityToken(
723-
PrepareIntegrityTokenRequest.builder()
724-
.setCloudProjectNumber(cloudProjectNumber)
725-
.build()
726-
).addOnSuccessListener { tokenProvider ->
727-
integrityTokenProvider = tokenProvider
728-
Log.i("AppAttestation", "Prepared Google Play Integrity Token Provider: '${tokenProvider}'.")
729-
}.addOnFailureListener { exception ->
730-
Log.e("AppAttestation", "Failed to prepare Google Play Integrity Token Provider: '${exception.message}'.")
731-
}
732-
}
733-
}
734-
735-
/**
736-
* A simple proof-of-concept for fetching the Salesforce App Attestation
737-
* ECA Plug-In's "Challenge".
738-
* @return The Salesforce App Attestation ECA Plug-In's "Challenge"
739-
*/
740-
internal fun testSalesforceMobileAppAttestationChallengeRequest(): String {
741-
// Create the Salesforce App Attestation Challenge API client and fetch a new challenge.
742-
val appAttestationChallengeApiClient = AppAttestationChallengeApiClient(
743-
apiHostName = "msdkappattestationtestorg.test1.my.pc-rnd.salesforce.com", // TODO: Replace with template placeholder. ECJ20260311
744-
restClient = clientManager.peekUnauthenticatedRestClient()
745-
)
746-
val salesforceAppAttestationChallenge = appAttestationChallengeApiClient.fetchChallenge(
747-
attestationId = deviceId,
748-
remoteConsumerKey = getBootConfig(this@SalesforceSDKManager.appContext).remoteAccessConsumerKey
749-
)
750-
751-
return salesforceAppAttestationChallenge
752-
}
753-
754-
/**
755-
* A simple proof-of-concept for fetching a Google Play Integrity API Token
756-
* using the Salesforce App Attestation ECA Plug-In's "Challenge" as the
757-
* Request Hash.
758-
* @return The Google Play Integrity API Token
759-
*/
760-
fun testOAuthAuthorizationAttestationRequest(): String? {
761-
// Guards.
762-
val integrityTokenProvider = integrityTokenProvider ?: return null
763-
764-
// Fetch the Salesforce Mobile App Attestation Challenge.
765-
val salesforceAppAttestationChallenge = testSalesforceMobileAppAttestationChallengeRequest()
766-
val salesforceAppAttestationChallengeHashByteArray = MessageDigest.getInstance("SHA-256")
767-
.digest(salesforceAppAttestationChallenge.toByteArray(UTF_8))
768-
val salesforceAppAttestationChallengeHashHexString = salesforceAppAttestationChallengeHashByteArray.joinToString("") { "%02x".format(it) }
769-
770-
// Request the Google Play Integrity Token.
771-
val integrityTokenResponse = integrityTokenProvider.request(
772-
StandardIntegrityTokenRequest.builder()
773-
.setRequestHash(salesforceAppAttestationChallengeHashHexString)
774-
.build()
775-
)
776-
val googlePlayIntegrityTask = integrityTokenResponse.addOnSuccessListener { response ->
777-
Log.i("AppAttestation", "Received Google Play Integrity Token: '${response.token()}'.")
778-
779-
}.addOnFailureListener { exception ->
780-
// If the app uses the same token provider for too long, the token provider can expire which results in the INTEGRITY_TOKEN_PROVIDER_INVALID error on the next token request. You should handle this error by requesting a new provider.
781-
Log.e("AppAttestation", "Failed To Receive Google Play Integrity Token: Message: '${exception.message}'.")
782-
783-
// TODO: Handle the error by requesting a new Google Play Integrity Token Provider. ECJ20260311
784-
}
785-
786-
// Wait for the Google Play Integrity API response and return the Base64-encoded Salesforce OAuth authorization attestation parameter JSON.
787-
runBlocking {
788-
googlePlayIntegrityTask.await()
789-
}
790-
return OAuthAuthorizationAttestation(
791-
attestationId = deviceId,
792-
attestationData = Base64.getEncoder().encodeToString(
793-
googlePlayIntegrityTask.getResult().token().encodeToByteArray()
794-
)
795-
).toBase64String()
796-
}
797-
798-
/**
799-
* A Salesforce OAuth 2.0 authorization "attestation" parameter.
800-
* @param attestationId The attestation id used when creating the Salesforce
801-
* Mobile App Attestation API Challenge. This is intended to be the
802-
* Salesforce Mobile SDK device id
803-
* @param attestationData The token provided by the Google Play Integrity API
804-
*/
805-
@Serializable
806-
internal data class OAuthAuthorizationAttestation(
807-
val attestationId: String,
808-
val attestationData: String,
809-
) {
810-
811-
/**
812-
* Returns a Base64-encoded JSON representation of this object
813-
*/
814-
fun toBase64String(): String? = Base64.getEncoder().encodeToString(Json.encodeToString(serializer(), this).encodeToByteArray())
815-
}
692+
fun createAppAttestationClient() = AppAttestationClient(
693+
appContext,
694+
deviceId,
695+
getBootConfig(getInstance().appContext).remoteAccessConsumerKey,
696+
clientManager.peekUnauthenticatedRestClient()
697+
)
816698

817699
/**
818700
* Optionally enables browser based login instead of web view login.

0 commit comments

Comments
 (0)