From b6feef4a39edaf2c225c8802a62d8449b6450173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Strzy=C5=BC?= Date: Tue, 8 Sep 2026 16:10:35 +0200 Subject: [PATCH 1/4] Add permission security snippets and region tags --- gradle/libs.versions.toml | 2 + security/build.gradle.kts | 1 + security/src/main/AndroidManifest.xml | 90 +++++++++++++++++++ .../security/permissions/CallerVerifier.kt | 43 +++++++++ .../permissions/FineGrainedBoundService.kt | 57 ++++++++++++ .../permissions/LocationPermissionSnippets.kt | 79 ++++++++++++++++ .../permissions/MediaPickerSnippets.kt | 52 +++++++++++ .../permissions/PermissionComponentStubs.kt | 58 ++++++++++++ .../permissions/PermissionErrorHandling.kt | 85 ++++++++++++++++++ .../permissions/ProtectedBroadcastSnippets.kt | 82 +++++++++++++++++ .../permissions/RuntimePermissionsActivity.kt | 73 +++++++++++++++ .../permissions/UriPermissionSnippets.kt | 43 +++++++++ .../src/main/res/values/security_certs.xml | 24 +++++ security/src/main/res/xml/allowed_peers.xml | 26 ++++++ 14 files changed, 715 insertions(+) create mode 100644 security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt create mode 100644 security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt create mode 100644 security/src/main/res/values/security_certs.xml create mode 100644 security/src/main/res/xml/allowed_peers.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa88b1fd3..c3c7861f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ androidx-corepip = "1.0.0-alpha02" androidx-credentials = "1.7.0-alpha03" androidx-credentials-play-services-auth = "1.7.0-alpha03" androidx-credentials-providerevents = "1.0.0-alpha06" +androidx-security-app-authenticator = "1.0.0-alpha02" androidx-emoji2-views = "1.6.0" androidx-fragment = "1.9.0" androidx-glance-appwidget = "1.3.0-alpha02" @@ -254,6 +255,7 @@ androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref androidx-room3-rxjava3 = { module = "androidx.room3:room3-rxjava3", version.ref = "room3" } androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" } androidx-room3-testing = { module = "androidx.room3:room3-testing", version.ref = "room3" } +androidx-security-app-authenticator = { module = "androidx.security:security-app-authenticator", version.ref = "androidx-security-app-authenticator" } androidx-sqlite-async = { module = "androidx.sqlite:sqlite-async", version.ref = "sqlite" } androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } diff --git a/security/build.gradle.kts b/security/build.gradle.kts index 4ec5ac2d3..1b8e9bc90 100644 --- a/security/build.gradle.kts +++ b/security/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.appcompat) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.security.app.authenticator) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.foundation) diff --git a/security/src/main/AndroidManifest.xml b/security/src/main/AndroidManifest.xml index 1a6958e5a..f93345400 100644 --- a/security/src/main/AndroidManifest.xml +++ b/security/src/main/AndroidManifest.xml @@ -29,6 +29,40 @@ android:name="com.example.snippets.permission.WRITE_DATA" android:protectionLevel="signature" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt b/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt new file mode 100644 index 000000000..bd6d5a749 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.content.Context +import androidx.security.app.authenticator.AppAuthenticator +import com.example.snippets.security.R + +// [START android_security_caller_verifier_app_authenticator] +class CallerVerifier(private val context: Context) { + private val appAuthenticator: AppAuthenticator by lazy { + AppAuthenticator.createFromResource(context, R.xml.allowed_peers) + } + + /** + * Verifies the caller's identity. + * + * @param callingPackage The package name of the caller. + * @throws SecurityException If the caller is not authorized. + */ + fun enforceCaller(callingPackage: String) { + try { + appAuthenticator.enforceAppIdentity(callingPackage) + } catch (e: SecurityException) { + throw SecurityException("Caller $callingPackage is not authorized", e) + } + } +} +// [END android_security_caller_verifier_app_authenticator] diff --git a/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt b/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt new file mode 100644 index 000000000..39fb122b4 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.IBinder + +// Stub interface representing AIDL generated interface +interface IMyService { + fun getData(): String + fun modifyData(newData: String) +} + +// [START android_security_service_enforce_calling_permission] +class FineGrainedBoundService : Service() { + + private val binder = object : Binder(), IMyService { + override fun getData(): String { + // Enforce read permission on the caller + enforceCallingPermission( + "com.example.snippets.permission.READ_DATA", + "Caller does not have READ_DATA permission" + ) + return "Sensitive data from service" + } + + override fun modifyData(newData: String) { + // Enforce write permission on the caller + enforceCallingPermission( + "com.example.snippets.permission.WRITE_DATA", + "Caller does not have WRITE_DATA permission" + ) + // Perform modification + } + } + + override fun onBind(intent: Intent): IBinder { + return binder + } +} +// [END android_security_service_enforce_calling_permission] diff --git a/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt new file mode 100644 index 000000000..3613f24bc --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.Manifest +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts + +// [START android_security_sequential_location_permission] +class LocationPermissionActivity : ComponentActivity() { + + // Step 1: Register launcher for ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION + private val foregroundLocationLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val fineLocationGranted = + permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) + val coarseLocationGranted = + permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) + + if (fineLocationGranted || coarseLocationGranted) { + // Foreground location access granted. Now request background location in a distinct step. + requestBackgroundLocation() + } else { + onForegroundLocationDenied() + } + } + + // Step 2: Register a separate launcher for ACCESS_BACKGROUND_LOCATION + private val backgroundLocationLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + onBackgroundLocationGranted() + } else { + onBackgroundLocationDenied() + } + } + + // Request foreground permissions first + fun requestForegroundLocation() { + foregroundLocationLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + } + + // Request background location only after foreground permissions have been granted + fun requestBackgroundLocation() { + backgroundLocationLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + + private fun onForegroundLocationDenied() { + // Handle foreground location denial + } + + private fun onBackgroundLocationGranted() { + // Start background location updates + } + + private fun onBackgroundLocationDenied() { + // Handle background location denial; proceed with foreground-only features + } +} +// [END android_security_sequential_location_permission] diff --git a/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt new file mode 100644 index 000000000..60a3e48dc --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts + +// [START android_security_photo_picker_request] +class MediaPickerActivity : ComponentActivity() { + + // Registers a photo picker activity launcher in single-select mode. + // The photo picker provides safe, direct access to media items without requiring + // READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permissions. + private val launcher = + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? -> + if (uri != null) { + // Access the media directly using the returned URI without storage permissions. + handleSelectedImage(uri) + } else { + handleNoImageSelected() + } + } + + fun selectPhoto() { + launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + + private fun handleSelectedImage(uri: Uri) { + // Direct URI access without requesting storage permissions + } + + private fun handleNoImageSelected() { + // Picker was cancelled or no photo was selected + } +} +// [END android_security_photo_picker_request] diff --git a/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt b/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt new file mode 100644 index 000000000..d20307992 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.app.Service +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Intent +import android.database.Cursor +import android.net.Uri +import android.os.IBinder + +class SecureDataService : Service() { + override fun onBind(intent: Intent?): IBinder? = null +} + +class PartnerDataService : Service() { + override fun onBind(intent: Intent?): IBinder? = null +} + +class PartnerSecureDataProvider : ContentProvider() { + override fun onCreate(): Boolean = true + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int = 0 +} diff --git a/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt new file mode 100644 index 000000000..1c806bf86 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.os.Binder +import android.os.Process +import android.util.Base64 +import android.util.Log + +class PermissionErrorHandling(private val context: Context) { + + // [START android_security_permission_error_handling] + fun safeLocationAccess(locationManager: LocationManager) { + val pm = context.packageManager + try { + @Suppress("DEPRECATION") + val info = pm.getPackageInfo("com.unknown.app", PackageManager.GET_SIGNATURES) + } catch (e: PackageManager.NameNotFoundException) { + Log.e("PERMISSION_ERROR", "Requested package information is not installed.", e) + // Abort actions calling the target package + } + + try { + // Calling API requiring ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION + val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (lastLocation != null) { + displayLocationData(lastLocation) + } + } catch (e: SecurityException) { + Log.w("PERMISSION_DENIED", "Attempted to access location without GPS permission", e) + // Fallback logic: Use a default location or guide user to enter location manually + useDefaultLocation() + } + } + // [END android_security_permission_error_handling] + + // [START android_security_caller_signature_verification] + fun verifyCallerIdentity(trustedSha256: String) { + val callingUid = Binder.getCallingUid() + if (callingUid == Process.myUid()) { + return + } + + val pm = context.packageManager + val packages = pm.getPackagesForUid(callingUid) + if (packages.isNullOrEmpty()) { + throw SecurityException("Unknown caller UID: $callingUid") + } + + val callingPackage = packages[0] + val trustedSha256Raw = Base64.decode(trustedSha256, Base64.DEFAULT) + // API 28+ handles signing key lineage and avoids manual cert parsing + val isTrusted = pm.hasSigningCertificate( + callingPackage, + trustedSha256Raw, + PackageManager.CERT_INPUT_SHA256 + ) + + if (!isTrusted) { + throw SecurityException("Caller signature verification failed for $callingPackage") + } + } + // [END android_security_caller_signature_verification] + + private fun displayLocationData(location: Location) {} + private fun useDefaultLocation() {} +} diff --git a/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt new file mode 100644 index 000000000..f593240fd --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.app.BroadcastOptions +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi + +// [START android_security_protected_broadcast_send] +fun sendProtectedBroadcast(context: Context) { + val intent = Intent("com.example.snippets.ACTION_SECRET_UPDATE") + // Enforce permission requirements during broadcast dispatch + context.sendBroadcast(intent, "com.example.snippets.permission.ACCESS_SECURE_API") +} +// [END android_security_protected_broadcast_send] + +// [START android_security_broadcast_sender_identity] +@Suppress("ObsoleteSdkInt") +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +fun sendBroadcastWithIdentity(context: Context) { + val intent = Intent("com.example.snippets.ACTION_SECRET_UPDATE") + + // Opt-in to sharing sender identity + val options = BroadcastOptions.makeBasic().apply { + setShareIdentityEnabled(true) + } + + context.sendBroadcast( + intent, + "com.example.snippets.permission.ACCESS_SECURE_API", + options.toBundle() + ) +} +// [END android_security_broadcast_sender_identity] + +// [START android_security_broadcast_receiver_verify_identity] +class MyProtectedReceiver : BroadcastReceiver() { + @Suppress("ObsoleteSdkInt") + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == "com.example.snippets.ACTION_SECRET_UPDATE") { + // Retrieve the sender's package name on Android 14+ + val senderPackage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + sentFromPackage + } else { + null + } + + if (senderPackage != null) { + try { + // Verify the sender's identity using AppAuthenticator + CallerVerifier(context).enforceCaller(senderPackage) + processUpdate(intent) + } catch (e: SecurityException) { + Log.w("SECURITY_ALERT", "Untrusted broadcast sender: $senderPackage", e) + } + } else { + Log.w("SECURITY_ALERT", "Broadcast received without sender identity") + } + } + } + + private fun processUpdate(intent: Intent) {} +} +// [END android_security_broadcast_receiver_verify_identity] diff --git a/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt new file mode 100644 index 000000000..ec3e4e892 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat + +// [START android_security_runtime_permission_request] +class CameraActivity : ComponentActivity() { + + private val permission = Manifest.permission.CAMERA + + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + startCameraPreview() + } else { + if (!shouldShowRequestPermissionRationale(permission)) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } else { + showPermissionDeniedFeedback() + } + } + } + + fun checkAndLaunchCamera(permission: String) { + when { + ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> { + startCameraPreview() + } + // Handle the case where the user denied the permission before. + shouldShowRequestPermissionRationale(permission) -> { + showPermissionRationaleAndRetry(permission) + } + else -> { + // Request the permission for the first time. + requestPermissionLauncher.launch(permission) + } + } + } + + private fun showPermissionRationaleAndRetry(permission: String) { + // UI logic to explain why the permission is needed, then re-trigger launch: + // requestPermissionLauncher.launch(permission) + } + + private fun startCameraPreview() { /* Camera preview initialization logic */ } + private fun showPermissionDeniedFeedback() { /* UI warning indicating permission is required */ } +} +// [END android_security_runtime_permission_request] diff --git a/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt new file mode 100644 index 000000000..37630c8e2 --- /dev/null +++ b/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.security.permissions + +import android.content.Context +import android.content.Intent +import android.content.UriPermission +import android.net.Uri + +// [START android_security_uri_permission_management] +fun grantScopedUriAccess(context: Context, targetPackage: String, uri: Uri) { + context.grantUriPermission( + targetPackage, + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) +} + +fun getActivePersistedPermissions(context: Context): List { + return context.contentResolver.persistedUriPermissions +} + +fun revokeScopedUriAccess(context: Context, uri: Uri) { + context.revokeUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) +} +// [END android_security_uri_permission_management] diff --git a/security/src/main/res/values/security_certs.xml b/security/src/main/res/values/security_certs.xml new file mode 100644 index 000000000..d0c6fb5b4 --- /dev/null +++ b/security/src/main/res/values/security_certs.xml @@ -0,0 +1,24 @@ + + + + + + + 103938ee4537e59e8ee792f654504fb8346fc6b346d0bbc4415fc339fcfc8ec1 + + + diff --git a/security/src/main/res/xml/allowed_peers.xml b/security/src/main/res/xml/allowed_peers.xml new file mode 100644 index 000000000..39391c7ff --- /dev/null +++ b/security/src/main/res/xml/allowed_peers.xml @@ -0,0 +1,26 @@ + + + + + + + + A1B2C3D4E5F60708090A0B0C0D0E0F1011121314151617181920212223242526 + + + + From 8adf282493b1ec5dac55c55b487588f5fd21aed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Strzy=C5=BC?= Date: Thu, 10 Sep 2026 10:53:33 +0200 Subject: [PATCH 2/4] Align permission security broadcast snippets with skill guide --- security/src/main/AndroidManifest.xml | 38 ++++---- .../security/permissions/CallerVerifier.kt | 93 +++++++++++++++---- .../permissions/FineGrainedBoundService.kt | 33 ++++--- .../permissions/LocationPermissionSnippets.kt | 49 ++++------ .../permissions/MediaPickerSnippets.kt | 26 ++---- .../permissions/PermissionComponentStubs.kt | 4 +- .../permissions/PermissionErrorHandling.kt | 71 +++++--------- .../permissions/ProtectedBroadcastSnippets.kt | 66 ++++++------- .../permissions/RuntimePermissionsActivity.kt | 37 ++++---- .../permissions/UriPermissionSnippets.kt | 45 ++++++--- .../src/main/res/values/security_certs.xml | 4 +- 11 files changed, 244 insertions(+), 222 deletions(-) diff --git a/security/src/main/AndroidManifest.xml b/security/src/main/AndroidManifest.xml index f93345400..659b3cdab 100644 --- a/security/src/main/AndroidManifest.xml +++ b/security/src/main/AndroidManifest.xml @@ -22,12 +22,18 @@ android:protectionLevel="signature" /> + + + + - - - - - - - - + android:permission="com.example.permissions.RECEIVE_SECRET_UPDATE"> - + + android:readPermission="com.example.snippets.permission.READ_DATA" + android:writePermission="com.example.snippets.permission.WRITE_DATA" + android:grantUriPermissions="true"> + + diff --git a/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt b/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt index bd6d5a749..bf471c925 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/CallerVerifier.kt @@ -17,27 +17,84 @@ package com.example.snippets.security.permissions import android.content.Context -import androidx.security.app.authenticator.AppAuthenticator -import com.example.snippets.security.R +import android.content.pm.PackageManager +import android.os.Binder +import android.os.Build +import android.util.Log -// [START android_security_caller_verifier_app_authenticator] -class CallerVerifier(private val context: Context) { - private val appAuthenticator: AppAuthenticator by lazy { - AppAuthenticator.createFromResource(context, R.xml.allowed_peers) +// [START android_security_caller_verifier] +// [START android_security_caller_signature_verification] +object CallerVerifier { + private const val TRUSTED_PARTNER_SHA256 = + "A1B2C3D4E5F60708090A0B0C0D0E0F1011121314151617181920212223242526" + + fun isCallerAuthorized(context: Context): Boolean { + val callingUid = Binder.getCallingUid() + if (callingUid == android.os.Process.myUid()) return true + + val pm = context.packageManager + // Modern API 28+ check by UID: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val certBytes = hexStringToByteArray(TRUSTED_PARTNER_SHA256) + if (pm.hasSigningCertificate(callingUid, certBytes, PackageManager.CERT_INPUT_SHA256)) { + return true + } + } + + // Fallback for legacy APIs: + val callingPackages = pm.getPackagesForUid(callingUid) ?: return false + for (pkg in callingPackages) { + if (verifyPackageSignature(pm, pkg)) { + return true + } + } + return false + } + + @Suppress("DEPRECATION") + private fun verifyPackageSignature(pm: PackageManager, packageName: String): Boolean { + return try { + val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) + val signatures = packageInfo.signatures ?: return false + for (sig in signatures) { + val digest = java.security.MessageDigest.getInstance("SHA-256").digest(sig.toByteArray()) + val hex = digest.joinToString("") { "%02X".format(it) } + if (hex.equals(TRUSTED_PARTNER_SHA256, ignoreCase = true)) return true + } + false + } catch (e: PackageManager.NameNotFoundException) { + false + } } - /** - * Verifies the caller's identity. - * - * @param callingPackage The package name of the caller. - * @throws SecurityException If the caller is not authorized. - */ - fun enforceCaller(callingPackage: String) { - try { - appAuthenticator.enforceAppIdentity(callingPackage) - } catch (e: SecurityException) { - throw SecurityException("Caller $callingPackage is not authorized", e) + private fun hexStringToByteArray(s: String): ByteArray { + val len = s.length + val data = ByteArray(len / 2) + for (i in 0 until len step 2) { + data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() } + return data + } + + private fun processSensitiveData(payload: String?) { + Log.d("SecureDataService", "Processing data safely: $payload") + } +} +// [END android_security_caller_signature_verification] +// [END android_security_caller_verifier] + +// Placeholder helper for compilation of vulnerable anti-pattern snippet +object SignatureUtils { + fun verifyPartnerPackage(context: Context, packageName: String): Boolean = true +} + +fun Context.vulnerableCallerCheck(intent: android.content.Intent?) { + // [START android_security_caller_spoofing_vulnerable] + // VULNERABLE PATTERN: DO NOT DO THIS + val callingPackage = intent?.getStringExtra("calling_package") + if (callingPackage != null && SignatureUtils.verifyPartnerPackage(this, callingPackage)) { + // A malicious app passes "com.example.partner" in the extra. + // The signature check verifies the installed partner on disk, but the CALLER was malicious! } + // [END android_security_caller_spoofing_vulnerable] } -// [END android_security_caller_verifier_app_authenticator] diff --git a/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt b/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt index 39fb122b4..816151cfa 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt @@ -20,38 +20,43 @@ import android.app.Service import android.content.Intent import android.os.Binder import android.os.IBinder +import android.util.Log // Stub interface representing AIDL generated interface interface IMyService { fun getData(): String fun modifyData(newData: String) + + abstract class Stub : Binder(), IMyService } // [START android_security_service_enforce_calling_permission] -class FineGrainedBoundService : Service() { +class SecureBoundService : Service() { - private val binder = object : Binder(), IMyService { + private val binder = object : IMyService.Stub() { override fun getData(): String { - // Enforce read permission on the caller - enforceCallingPermission( - "com.example.snippets.permission.READ_DATA", - "Caller does not have READ_DATA permission" - ) - return "Sensitive data from service" + // Read-only operation guarded by manifest-level permission + return "Confidential Data" } override fun modifyData(newData: String) { - // Enforce write permission on the caller - enforceCallingPermission( + // MUST use enforceCallingPermission or checkCallingPermission. + // NEVER use checkCallingOrSelfPermission or enforceCallingOrSelfPermission. + this@SecureBoundService.enforceCallingPermission( "com.example.snippets.permission.WRITE_DATA", - "Caller does not have WRITE_DATA permission" + "Caller lacks WRITE_DATA permission" ) - // Perform modification + updateInternalState(newData) + Log.d("SecureBoundService", "Data modified to: $newData with proper WRITE_DATA permission check") } } - override fun onBind(intent: Intent): IBinder { - return binder + override fun onBind(intent: Intent?): IBinder = binder + + private fun updateInternalState(data: String) { + // Internal state update logic } } // [END android_security_service_enforce_calling_permission] + +typealias FineGrainedBoundService = SecureBoundService diff --git a/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt index 3613f24bc..f4a1921af 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/LocationPermissionSnippets.kt @@ -20,36 +20,18 @@ import android.Manifest import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts -// [START android_security_sequential_location_permission] class LocationPermissionActivity : ComponentActivity() { - // Step 1: Register launcher for ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION + // [START android_security_sequential_location_permission] private val foregroundLocationLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - val fineLocationGranted = - permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) - val coarseLocationGranted = - permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) - - if (fineLocationGranted || coarseLocationGranted) { - // Foreground location access granted. Now request background location in a distinct step. - requestBackgroundLocation() - } else { - onForegroundLocationDenied() + val fineGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false + val coarseGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false + if (fineGranted || coarseGranted) { + startForegroundLocationUpdates() } } - // Step 2: Register a separate launcher for ACCESS_BACKGROUND_LOCATION - private val backgroundLocationLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> - if (isGranted) { - onBackgroundLocationGranted() - } else { - onBackgroundLocationDenied() - } - } - - // Request foreground permissions first fun requestForegroundLocation() { foregroundLocationLauncher.launch( arrayOf( @@ -59,21 +41,24 @@ class LocationPermissionActivity : ComponentActivity() { ) } - // Request background location only after foreground permissions have been granted + // Background location requested only AFTER foreground is granted and user explicitly opts in: + private val backgroundLocationLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + startBackgroundTracking() + } + } + // [END android_security_sequential_location_permission] + fun requestBackgroundLocation() { backgroundLocationLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } - private fun onForegroundLocationDenied() { - // Handle foreground location denial + private fun startForegroundLocationUpdates() { + // Start foreground location updates } - private fun onBackgroundLocationGranted() { + private fun startBackgroundTracking() { // Start background location updates } - - private fun onBackgroundLocationDenied() { - // Handle background location denial; proceed with foreground-only features - } } -// [END android_security_sequential_location_permission] diff --git a/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt index 60a3e48dc..5960be757 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/MediaPickerSnippets.kt @@ -21,32 +21,24 @@ import androidx.activity.ComponentActivity import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -// [START android_security_photo_picker_request] class MediaPickerActivity : ComponentActivity() { - // Registers a photo picker activity launcher in single-select mode. - // The photo picker provides safe, direct access to media items without requiring - // READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permissions. - private val launcher = - registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? -> + // [START android_security_photo_picker_request] + private val photoPickerLauncher = + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> if (uri != null) { - // Access the media directly using the returned URI without storage permissions. - handleSelectedImage(uri) - } else { - handleNoImageSelected() + handleImageUri(uri) } } fun selectPhoto() { - launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + photoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) } + // [END android_security_photo_picker_request] - private fun handleSelectedImage(uri: Uri) { + private fun handleImageUri(uri: Uri) { // Direct URI access without requesting storage permissions } - - private fun handleNoImageSelected() { - // Picker was cancelled or no photo was selected - } } -// [END android_security_photo_picker_request] diff --git a/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt b/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt index d20307992..8b844d71b 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/PermissionComponentStubs.kt @@ -32,7 +32,7 @@ class PartnerDataService : Service() { override fun onBind(intent: Intent?): IBinder? = null } -class PartnerSecureDataProvider : ContentProvider() { +open class SecureDataProvider : ContentProvider() { override fun onCreate(): Boolean = true override fun query( @@ -56,3 +56,5 @@ class PartnerSecureDataProvider : ContentProvider() { selectionArgs: Array? ): Int = 0 } + +class PartnerSecureDataProvider : SecureDataProvider() diff --git a/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt index 1c806bf86..0af952fd7 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt @@ -16,70 +16,49 @@ package com.example.snippets.security.permissions +import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.location.Location import android.location.LocationManager -import android.os.Binder -import android.os.Process -import android.util.Base64 import android.util.Log +import androidx.activity.ComponentActivity +import androidx.core.content.ContextCompat -class PermissionErrorHandling(private val context: Context) { +class PermissionErrorHandling : ComponentActivity() { + + private val locationManager: LocationManager by lazy { + getSystemService(Context.LOCATION_SERVICE) as LocationManager + } // [START android_security_permission_error_handling] - fun safeLocationAccess(locationManager: LocationManager) { - val pm = context.packageManager - try { - @Suppress("DEPRECATION") - val info = pm.getPackageInfo("com.unknown.app", PackageManager.GET_SIGNATURES) - } catch (e: PackageManager.NameNotFoundException) { - Log.e("PERMISSION_ERROR", "Requested package information is not installed.", e) - // Abort actions calling the target package + fun performLocationAccess() { + val fine = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + val coarse = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) + if (fine != PackageManager.PERMISSION_GRANTED && coarse != PackageManager.PERMISSION_GRANTED) { + requestForegroundLocation() + return } try { - // Calling API requiring ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION - val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - if (lastLocation != null) { - displayLocationData(lastLocation) - } + val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + processLocation(location) } catch (e: SecurityException) { - Log.w("PERMISSION_DENIED", "Attempted to access location without GPS permission", e) - // Fallback logic: Use a default location or guide user to enter location manually - useDefaultLocation() + Log.e("LocationAccess", "Permission revoked at runtime", e) } } // [END android_security_permission_error_handling] - // [START android_security_caller_signature_verification] - fun verifyCallerIdentity(trustedSha256: String) { - val callingUid = Binder.getCallingUid() - if (callingUid == Process.myUid()) { - return - } - - val pm = context.packageManager - val packages = pm.getPackagesForUid(callingUid) - if (packages.isNullOrEmpty()) { - throw SecurityException("Unknown caller UID: $callingUid") - } - - val callingPackage = packages[0] - val trustedSha256Raw = Base64.decode(trustedSha256, Base64.DEFAULT) - // API 28+ handles signing key lineage and avoids manual cert parsing - val isTrusted = pm.hasSigningCertificate( - callingPackage, - trustedSha256Raw, - PackageManager.CERT_INPUT_SHA256 - ) + fun safeLocationAccess(locationManager: LocationManager) { + performLocationAccess() + } - if (!isTrusted) { - throw SecurityException("Caller signature verification failed for $callingPackage") + fun verifyCallerIdentity(trustedSha256: String) { + if (!CallerVerifier.isCallerAuthorized(this)) { + throw SecurityException("Caller signature verification failed") } } - // [END android_security_caller_signature_verification] - private fun displayLocationData(location: Location) {} - private fun useDefaultLocation() {} + private fun requestForegroundLocation() {} + private fun processLocation(location: Location?) {} } diff --git a/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt index f593240fd..8ffb8e475 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt @@ -21,62 +21,50 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi // [START android_security_protected_broadcast_send] fun sendProtectedBroadcast(context: Context) { - val intent = Intent("com.example.snippets.ACTION_SECRET_UPDATE") - // Enforce permission requirements during broadcast dispatch - context.sendBroadcast(intent, "com.example.snippets.permission.ACCESS_SECURE_API") + val intent = Intent("com.example.permissions.ACTION_SECRET_UPDATE").apply { + setPackage("com.example.partner") + } + context.sendBroadcast(intent, "com.example.permissions.RECEIVE_SECRET_UPDATE") } // [END android_security_protected_broadcast_send] -// [START android_security_broadcast_sender_identity] -@Suppress("ObsoleteSdkInt") -@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -fun sendBroadcastWithIdentity(context: Context) { - val intent = Intent("com.example.snippets.ACTION_SECRET_UPDATE") - - // Opt-in to sharing sender identity - val options = BroadcastOptions.makeBasic().apply { - setShareIdentityEnabled(true) +fun Context.sendBroadcastWithIdentity() { + // [START android_security_broadcast_sender_identity] + // Sender: Enforce permission and share identity + val intent = Intent("com.example.permissions.ACTION_SECRET_UPDATE").apply { + setPackage("com.example.partner") // Explicit target } + val isUdc = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + val options = if (isUdc) { + BroadcastOptions.makeBasic().apply { + setShareIdentityEnabled(true) + }.toBundle() + } else null - context.sendBroadcast( - intent, - "com.example.snippets.permission.ACCESS_SECURE_API", - options.toBundle() - ) + sendBroadcast(intent, "com.example.permissions.RECEIVE_SECRET_UPDATE", options) + // [END android_security_broadcast_sender_identity] } -// [END android_security_broadcast_sender_identity] // [START android_security_broadcast_receiver_verify_identity] -class MyProtectedReceiver : BroadcastReceiver() { - @Suppress("ObsoleteSdkInt") +// Receiver: Validate sender on Android 14+ +class ProtectedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == "com.example.snippets.ACTION_SECRET_UPDATE") { - // Retrieve the sender's package name on Android 14+ - val senderPackage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - sentFromPackage - } else { - null - } - - if (senderPackage != null) { - try { - // Verify the sender's identity using AppAuthenticator - CallerVerifier(context).enforceCaller(senderPackage) - processUpdate(intent) - } catch (e: SecurityException) { - Log.w("SECURITY_ALERT", "Untrusted broadcast sender: $senderPackage", e) + if (intent.action == "com.example.permissions.ACTION_SECRET_UPDATE") { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val sender = sentFromPackage + if (sender != null && sender != "com.example.trusted_sender") { + return // Reject unauthorized sender } - } else { - Log.w("SECURITY_ALERT", "Broadcast received without sender identity") } + processUpdate(intent) } } private fun processUpdate(intent: Intent) {} } // [END android_security_broadcast_receiver_verify_identity] + +typealias MyProtectedReceiver = ProtectedReceiver diff --git a/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt index ec3e4e892..158e0868a 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt @@ -21,53 +21,56 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.provider.Settings +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat // [START android_security_runtime_permission_request] -class CameraActivity : ComponentActivity() { +class MainActivity : ComponentActivity() { - private val permission = Manifest.permission.CAMERA - - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + private val cameraLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { startCameraPreview() } else { - if (!shouldShowRequestPermissionRationale(permission)) { + if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + // User selected 'Don't ask again' or permanently denied. + // Direct user to Application Details Settings. val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.fromParts("package", packageName, null) } startActivity(intent) } else { - showPermissionDeniedFeedback() + showSnackbar("Camera permission is required to preview camera feed.") } } } - fun checkAndLaunchCamera(permission: String) { + fun requestCameraPermissionSafely() { + val permission = Manifest.permission.CAMERA when { ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> { startCameraPreview() } - // Handle the case where the user denied the permission before. shouldShowRequestPermissionRationale(permission) -> { - showPermissionRationaleAndRetry(permission) + showSnackbar("Camera permission is needed to preview camera feed.") + cameraLauncher.launch(permission) } else -> { - // Request the permission for the first time. - requestPermissionLauncher.launch(permission) + cameraLauncher.launch(permission) } } } - private fun showPermissionRationaleAndRetry(permission: String) { - // UI logic to explain why the permission is needed, then re-trigger launch: - // requestPermissionLauncher.launch(permission) + private fun startCameraPreview() { + Log.d("MainActivity", "Camera preview started") } - private fun startCameraPreview() { /* Camera preview initialization logic */ } - private fun showPermissionDeniedFeedback() { /* UI warning indicating permission is required */ } + private fun showSnackbar(msg: String) { + Log.i("MainActivity", msg) + } } // [END android_security_runtime_permission_request] + +typealias CameraActivity = MainActivity diff --git a/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt index 37630c8e2..0c652c240 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/UriPermissionSnippets.kt @@ -22,22 +22,37 @@ import android.content.UriPermission import android.net.Uri // [START android_security_uri_permission_management] -fun grantScopedUriAccess(context: Context, targetPackage: String, uri: Uri) { - context.grantUriPermission( - targetPackage, - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) -} +object UriPermissionManager { -fun getActivePersistedPermissions(context: Context): List { - return context.contentResolver.persistedUriPermissions -} + /** Grants temporary read access to a specific URI for a partner package. */ + fun grantScopedUriAccess(context: Context, targetPackage: String, uri: Uri) { + context.grantUriPermission( + targetPackage, + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + + /** Audits active persisted URI permissions held by the application. */ + fun getActivePersistedPermissions(context: Context): List { + return context.contentResolver.persistedUriPermissions + } -fun revokeScopedUriAccess(context: Context, uri: Uri) { - context.revokeUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) + /** Revokes temporary URI access when data transfer completes. */ + fun revokeScopedUriAccess(context: Context, uri: Uri) { + context.revokeUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } } // [END android_security_uri_permission_management] + +fun grantScopedUriAccess(context: Context, targetPackage: String, uri: Uri) = + UriPermissionManager.grantScopedUriAccess(context, targetPackage, uri) + +fun getActivePersistedPermissions(context: Context): List = + UriPermissionManager.getActivePersistedPermissions(context) + +fun revokeScopedUriAccess(context: Context, uri: Uri) = + UriPermissionManager.revokeScopedUriAccess(context, uri) diff --git a/security/src/main/res/values/security_certs.xml b/security/src/main/res/values/security_certs.xml index d0c6fb5b4..09cf434a0 100644 --- a/security/src/main/res/values/security_certs.xml +++ b/security/src/main/res/values/security_certs.xml @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. --> + - 103938ee4537e59e8ee792f654504fb8346fc6b346d0bbc4415fc339fcfc8ec1 - + From 80428a3aac2157698e51191b91b2727be24f7e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Strzy=C5=BC?= Date: Mon, 14 Sep 2026 15:11:10 -0600 Subject: [PATCH 3/4] Address review comments for security permission snippets --- gradle/libs.versions.toml | 2 -- security/build.gradle.kts | 1 - security/src/main/AndroidManifest.xml | 15 ++++++++++----- .../security/permissions/CallerVerifier.kt | 5 ----- .../permissions/PermissionComponentStubs.kt | 2 -- .../permissions/PermissionErrorHandling.kt | 10 ++++++++-- ...dBroadcastSnippets.kt => ProtectedReceiver.kt} | 14 ++++++-------- .../permissions/RuntimePermissionsActivity.kt | 8 +++----- 8 files changed, 27 insertions(+), 30 deletions(-) rename security/src/main/java/com/example/snippets/security/permissions/{ProtectedBroadcastSnippets.kt => ProtectedReceiver.kt} (79%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3c7861f8..fa88b1fd3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,6 @@ androidx-corepip = "1.0.0-alpha02" androidx-credentials = "1.7.0-alpha03" androidx-credentials-play-services-auth = "1.7.0-alpha03" androidx-credentials-providerevents = "1.0.0-alpha06" -androidx-security-app-authenticator = "1.0.0-alpha02" androidx-emoji2-views = "1.6.0" androidx-fragment = "1.9.0" androidx-glance-appwidget = "1.3.0-alpha02" @@ -255,7 +254,6 @@ androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref androidx-room3-rxjava3 = { module = "androidx.room3:room3-rxjava3", version.ref = "room3" } androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" } androidx-room3-testing = { module = "androidx.room3:room3-testing", version.ref = "room3" } -androidx-security-app-authenticator = { module = "androidx.security:security-app-authenticator", version.ref = "androidx-security-app-authenticator" } androidx-sqlite-async = { module = "androidx.sqlite:sqlite-async", version.ref = "sqlite" } androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } diff --git a/security/build.gradle.kts b/security/build.gradle.kts index 1b8e9bc90..4ec5ac2d3 100644 --- a/security/build.gradle.kts +++ b/security/build.gradle.kts @@ -34,7 +34,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.appcompat) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.security.app.authenticator) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.foundation) diff --git a/security/src/main/AndroidManifest.xml b/security/src/main/AndroidManifest.xml index 659b3cdab..a1697fe4e 100644 --- a/security/src/main/AndroidManifest.xml +++ b/security/src/main/AndroidManifest.xml @@ -32,7 +32,7 @@ @@ -80,7 +80,7 @@ + android:permission="com.example.snippets.permission.RECEIVE_SECRET_UPDATE"> - + + android:exported="true" + android:permission="com.example.snippets.permission.READ_DATA"> + + + + ? ): Int = 0 } - -class PartnerSecureDataProvider : SecureDataProvider() diff --git a/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt index 0af952fd7..b0e4fcc4e 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/PermissionErrorHandling.kt @@ -40,8 +40,14 @@ class PermissionErrorHandling : ComponentActivity() { return } + val provider = if (fine == PackageManager.PERMISSION_GRANTED) { + LocationManager.GPS_PROVIDER + } else { + LocationManager.NETWORK_PROVIDER + } + try { - val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + val location = locationManager.getLastKnownLocation(provider) processLocation(location) } catch (e: SecurityException) { Log.e("LocationAccess", "Permission revoked at runtime", e) @@ -53,7 +59,7 @@ class PermissionErrorHandling : ComponentActivity() { performLocationAccess() } - fun verifyCallerIdentity(trustedSha256: String) { + fun verifyCallerIdentity() { if (!CallerVerifier.isCallerAuthorized(this)) { throw SecurityException("Caller signature verification failed") } diff --git a/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt b/security/src/main/java/com/example/snippets/security/permissions/ProtectedReceiver.kt similarity index 79% rename from security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt rename to security/src/main/java/com/example/snippets/security/permissions/ProtectedReceiver.kt index 8ffb8e475..eb5bc7682 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/ProtectedBroadcastSnippets.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/ProtectedReceiver.kt @@ -24,17 +24,17 @@ import android.os.Build // [START android_security_protected_broadcast_send] fun sendProtectedBroadcast(context: Context) { - val intent = Intent("com.example.permissions.ACTION_SECRET_UPDATE").apply { + val intent = Intent("com.example.snippets.permission.ACTION_SECRET_UPDATE").apply { setPackage("com.example.partner") } - context.sendBroadcast(intent, "com.example.permissions.RECEIVE_SECRET_UPDATE") + context.sendBroadcast(intent, "com.example.snippets.permission.RECEIVE_SECRET_UPDATE") } // [END android_security_protected_broadcast_send] fun Context.sendBroadcastWithIdentity() { // [START android_security_broadcast_sender_identity] // Sender: Enforce permission and share identity - val intent = Intent("com.example.permissions.ACTION_SECRET_UPDATE").apply { + val intent = Intent("com.example.snippets.permission.ACTION_SECRET_UPDATE").apply { setPackage("com.example.partner") // Explicit target } val isUdc = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE @@ -44,7 +44,7 @@ fun Context.sendBroadcastWithIdentity() { }.toBundle() } else null - sendBroadcast(intent, "com.example.permissions.RECEIVE_SECRET_UPDATE", options) + sendBroadcast(intent, "com.example.snippets.permission.RECEIVE_SECRET_UPDATE", options) // [END android_security_broadcast_sender_identity] } @@ -52,10 +52,10 @@ fun Context.sendBroadcastWithIdentity() { // Receiver: Validate sender on Android 14+ class ProtectedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == "com.example.permissions.ACTION_SECRET_UPDATE") { + if (intent.action == "com.example.snippets.permission.ACTION_SECRET_UPDATE") { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { val sender = sentFromPackage - if (sender != null && sender != "com.example.trusted_sender") { + if (sender != "com.example.trusted_sender") { return // Reject unauthorized sender } } @@ -66,5 +66,3 @@ class ProtectedReceiver : BroadcastReceiver() { private fun processUpdate(intent: Intent) {} } // [END android_security_broadcast_receiver_verify_identity] - -typealias MyProtectedReceiver = ProtectedReceiver diff --git a/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt index 158e0868a..1e2c41d94 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/RuntimePermissionsActivity.kt @@ -27,7 +27,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat // [START android_security_runtime_permission_request] -class MainActivity : ComponentActivity() { +class RuntimePermissionsActivity : ComponentActivity() { private val cameraLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> @@ -64,13 +64,11 @@ class MainActivity : ComponentActivity() { } private fun startCameraPreview() { - Log.d("MainActivity", "Camera preview started") + Log.d("RuntimePermissionsActivity", "Camera preview started") } private fun showSnackbar(msg: String) { - Log.i("MainActivity", msg) + Log.i("RuntimePermissionsActivity", msg) } } // [END android_security_runtime_permission_request] - -typealias CameraActivity = MainActivity From 99924d88aeab6a7768581c06fb68659b366e819c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Strzy=C5=BC?= Date: Wed, 16 Sep 2026 16:33:35 -0600 Subject: [PATCH 4/4] Address review comments for security permission snippets --- ...dBoundService.kt => SecureBoundService.kt} | 2 -- security/src/main/res/xml/allowed_peers.xml | 26 ------------------- 2 files changed, 28 deletions(-) rename security/src/main/java/com/example/snippets/security/permissions/{FineGrainedBoundService.kt => SecureBoundService.kt} (97%) delete mode 100644 security/src/main/res/xml/allowed_peers.xml diff --git a/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt b/security/src/main/java/com/example/snippets/security/permissions/SecureBoundService.kt similarity index 97% rename from security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt rename to security/src/main/java/com/example/snippets/security/permissions/SecureBoundService.kt index 816151cfa..4e60971cc 100644 --- a/security/src/main/java/com/example/snippets/security/permissions/FineGrainedBoundService.kt +++ b/security/src/main/java/com/example/snippets/security/permissions/SecureBoundService.kt @@ -58,5 +58,3 @@ class SecureBoundService : Service() { } } // [END android_security_service_enforce_calling_permission] - -typealias FineGrainedBoundService = SecureBoundService diff --git a/security/src/main/res/xml/allowed_peers.xml b/security/src/main/res/xml/allowed_peers.xml deleted file mode 100644 index 39391c7ff..000000000 --- a/security/src/main/res/xml/allowed_peers.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - A1B2C3D4E5F60708090A0B0C0D0E0F1011121314151617181920212223242526 - - - -