Skip to content
Open
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
91 changes: 91 additions & 0 deletions security/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,39 @@
android:protectionLevel="signature" />
<!--[END android_security_custom_permission_manifest]-->

<!--[START android_security_content_provider_read_write_manifest]-->
<permission
android:name="com.example.snippets.permission.READ_DATA"
android:protectionLevel="signature" />
<permission
android:name="com.example.snippets.permission.WRITE_DATA"
android:protectionLevel="signature" />
<!--[END android_security_content_provider_read_write_manifest]-->

<permission
android:name="com.example.snippets.permission.RECEIVE_SECRET_UPDATE"
android:protectionLevel="signature" />

<!--[START android_security_custom_permission_signature_manifest]-->
<permission
android:name="com.example.snippets.permission.ACCESS_SECURE_API"
android:protectionLevel="signature" />
<!--[END android_security_custom_permission_signature_manifest]-->

<!--[START android_security_custom_permission_known_signer_manifest]-->
<permission
android:name="com.example.snippets.permission.PARTNER_API"
android:protectionLevel="signature|knownSigner"
android:knownCerts="@array/trusted_partner_certs" />
<!--[END android_security_custom_permission_known_signer_manifest]-->

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<application>
<!--[START android_security_custom_permission_activity_manifest]-->
Expand All @@ -51,6 +78,70 @@
android:writePermission="com.example.snippets.permission.WRITE_DATA"
android:grantUriPermissions="false" />
<!--[END android_security_contentprovider_manifest]-->

<activity
android:name=".permissions.RuntimePermissionsActivity"
android:exported="false" />
<activity
android:name=".permissions.LocationPermissionActivity"
android:exported="false" />
<activity
android:name=".permissions.MediaPickerActivity"
android:exported="false" />

<!--[START android_security_custom_permission_service_manifest]-->
<service
android:name=".permissions.SecureDataService"
android:exported="true"
android:permission="com.example.snippets.permission.ACCESS_SECURE_API">
<intent-filter>
<action android:name="com.example.snippets.ACTION_GET_DATA" />
</intent-filter>
</service>
<!--[END android_security_custom_permission_service_manifest]-->

<!--[START android_security_known_signer_service_manifest]-->
<service
android:name=".permissions.PartnerDataService"
android:exported="true"
android:permission="com.example.snippets.permission.PARTNER_API">
<intent-filter>
<action android:name="com.example.snippets.ACTION_GET_PARTNER_DATA" />
</intent-filter>
</service>
<!--[END android_security_known_signer_service_manifest]-->

<!--[START android_security_protected_broadcast_receiver_manifest]-->
<receiver
android:name=".permissions.ProtectedReceiver"
android:exported="true"
android:permission="com.example.snippets.permission.RECEIVE_SECRET_UPDATE">
<intent-filter>
<action android:name="com.example.snippets.permission.ACTION_SECRET_UPDATE" />
</intent-filter>
</receiver>
<!--[END android_security_protected_broadcast_receiver_manifest]-->

<service
android:name=".permissions.SecureBoundService"
android:exported="true"
android:permission="com.example.snippets.permission.READ_DATA">
<intent-filter>
<action android:name="com.example.snippets.ACTION_BIND_SECURE_SERVICE" />
</intent-filter>
</service>

<!--[START android_security_provider_read_write_manifest]-->
<provider
android:name=".permissions.SecureDataProvider"
android:authorities="com.example.snippets.partnerprovider"
android:exported="true"
android:readPermission="com.example.snippets.permission.READ_DATA"
android:writePermission="com.example.snippets.permission.WRITE_DATA"
android:grantUriPermissions="true">
<grant-uri-permission android:pathPrefix="/shared/" />
</provider>
<!--[END android_security_provider_read_write_manifest]-->
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.os.Binder
import android.os.Build

// [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
}
}

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
}
}
// [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]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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
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 SecureBoundService : Service() {

private val binder = object : IMyService.Stub() {
override fun getData(): String {
// Read-only operation guarded by manifest-level permission
return "Confidential Data"
}

override fun modifyData(newData: String) {
// MUST use enforceCallingPermission or checkCallingPermission.
// NEVER use checkCallingOrSelfPermission or enforceCallingOrSelfPermission.
this@SecureBoundService.enforceCallingPermission(
"com.example.snippets.permission.WRITE_DATA",
"Caller lacks WRITE_DATA permission"
)
updateInternalState(newData)
Log.d("SecureBoundService", "Data modified to: $newData with proper WRITE_DATA permission check")
}
}

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: if you rename the file SecureBoundService.kt you wouldn't need this alias

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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

class LocationPermissionActivity : ComponentActivity() {

// [START android_security_sequential_location_permission]
private val foregroundLocationLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val fineGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
val coarseGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false
if (fineGranted || coarseGranted) {
startForegroundLocationUpdates()
}
}

fun requestForegroundLocation() {
foregroundLocationLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}

// 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 startForegroundLocationUpdates() {
// Start foreground location updates
}

private fun startBackgroundTracking() {
// Start background location updates
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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

class MediaPickerActivity : ComponentActivity() {

// [START android_security_photo_picker_request]
private val photoPickerLauncher =
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
handleImageUri(uri)
}
}

fun selectPhoto() {
photoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
// [END android_security_photo_picker_request]

private fun handleImageUri(uri: Uri) {
// Direct URI access without requesting storage permissions
}
}
Loading
Loading