4. Using Technology in a Safe Way

In Android, there are many specific security related issues that pertain only to certain technologies such as Activities or SQLite. If a developer does not have enough knowledge about each of the different security issues regarding each technology when designing and coding, then unexpected vulnerabilities may arise. This chapter will explain about the different scenarios that developers will need to know when using their application components.

4.1. Creating/Using Activities

4.1.1. Sample Code

The risks and countermeasures of using Activities differ depending on how that Activity is being used. In this section, we have classified 4 types of Activities based on how the Activity is being used. You can find out which type of activity you are supposed to create through the following chart shown below. Since the secure coding best practice varies according to how the activity is used, we will also explain about the implementation of the Activity as well.

Table 4.1.1 Definition of Activity Types
Type Definition
Private Activity An activity that cannot be launched by another application, and therefore is the safest activity
Public Activity An activity that is supposed to be used by an unspecified large number of applications.
Partner Activity An activity that can only be used by specific applications made by a trusted partner company.
In-house Activity An activity that can only be used by other in-house applications.
_images/image34.png

Fig. 4.1.1 Flow Figure to select Activity Type

4.1.1.1. Creating/Using Private Activities

Private Activities are Activities which cannot be launched by the other applications and therefore it is the safest Activity.

When using Activities that are only used within the application (Private Activity), as long as you use explicit Intents to the class then you do not have to worry about accidently sending it to any other application. However, there is a risk that a third party application can read an Intent that is used to start the Activity. Therefore it is necessary to make sure that if you are putting sensitive information inside an Intent used to start an Activity that you take countermeasures to make sure that it cannot be read by a malicious third party.

Sample code of how to create a Private Activity is shown below.

Points (Creating an Activity):

  1. Do not specify taskAffinity.
  2. Do not specify launchMode.
  3. Explicitly set the exported attribute to false.
  4. Handle the received intent carefully and securely, even though the intent was sent from the same application.
  5. Sensitive information can be sent since it is sending and receiving all within the same application.

To make the Activity private, set the “exported” attribute of the Activity element in the AndroidManifest.xml to false.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.privateactivity" >
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- Private activity -->
    <!-- *** POINT 1 *** Do not specify taskAffinity -->
    <!-- *** POINT 2 *** Do not specify launchMode -->
    <!-- *** POINT 3 *** Explicitly set the exported attribute to false. -->
    <activity
        android:name=".PrivateActivity"
        android:label="@string/app_name"
        android:exported="false" />
    
    <!-- Public activity launched by launcher -->
    <activity
        android:name=".PrivateUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
PrivateActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.privateactivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PrivateActivity extends Activity {
        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.private_activity);

        // *** POINT 4 *** Handle the received Intent carefully and securely,
        // even though the Intent was sent from the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = getIntent().getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Received param: \"%s\"", param),
                       Toast.LENGTH_LONG).show();
    }

    public void onReturnResultClick(View view) {
                
        // *** POINT 5 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Sensitive Info");
        setResult(RESULT_OK, intent);
        finish();
    }
}

Next, we show the sample code for how to use the Private Activity.

Point (Using an Activity):

  1. Do not set the FLAG_ACTIVITY_NEW_TASK flag for intents to start an activity.
  2. Use the explicit Intents with the class specified to call an activity in the same application.
  3. Sensitive information can be sent only by putExtra() since the destination activity is in the same application [1].
  4. Handle the received result data carefully and securely, even though the data comes from an activity within the same application.
[1]Caution: Unless points 1, 2 and 6 are abided by, there is a risk that Intents may be read by a third party. Please refer to 4.1.2.2. and 4.1.2.3. for more details.
PrivateUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.privateactivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PrivateUserActivity extends Activity {

    private static final int REQUEST_CODE = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
    }
    
    public void onUseActivityClick(View view) {
        
        // *** POINT 6 *** Do not set the FLAG_ACTIVITY_NEW_TASK flag
        // for intents to start an activity.
        // *** POINT 7 *** Use the explicit Intents with the class
        // specified to call an activity in the same application.
        Intent intent = new Intent(this, PrivateActivity.class);
        
        // *** POINT 8 *** Sensitive information can be sent only by putExtra()
        // since the destination activity is in the same application.
        intent.putExtra("PARAM", "Sensitive Info");
        
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode != RESULT_OK) return;
        
        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");
                        
            // *** POINT 9 *** Handle the received data carefully and securely,
            // even though the data comes from an activity within the same
            // application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            Toast.makeText(this,
                           String.format("Received result: \"%s\"", result),
                           Toast.LENGTH_LONG).show();
            break;
        }
    }
}

4.1.1.2. Creating/Using Public Activities

Public Activities are Activities which are supposed to be used by an unspecified large number of applications. It is necessary to be aware that Public Activities may receive Intents sent from malware.

In addition, when using Public Activities, it is necessary to be aware of the fact that malware can also receive or read the Intents sent to them.

The sample code to create a Public Activity is shown below.

Points (Creating an Activity):

  1. Explicitly set the exported attribute to true.
  2. Handle the received intent carefully and securely.
  3. When returning a result, do not include sensitive information.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.publicactivity" >
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- Public Activity -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to true. -->
    <activity
        android:name=".PublicActivity"
        android:label="@string/app_name" 
        android:exported="true">
      
      <!-- Define intent filter to receive an implicit intent for a specified action -->
      <intent-filter>
        <action android:name="org.jssec.android.activity.MY_ACTION" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
    </activity>
  </application>
</manifest>
PublicActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.publicactivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PublicActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // *** POINT 2 *** Handle the received intent carefully and securely.
        // Since this is a public activity, it is possible that the sending
        // application may be malware.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = getIntent().getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Received param: \"%s\"", param),
                       Toast.LENGTH_LONG).show();
    }

    public void onReturnResultClick(View view) {
                
        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // Since this is a public activity, it is possible that the receiving
        // application may be malware.
        // If there is no problem if the data gets received by malware,
        // then it can be returned as a result.
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Not Sensitive Info");
        setResult(RESULT_OK, intent);
        finish();
    }
}

Next, Herein after sample code of Public Activity user side.

Points (Using an Activity):

  1. Do not send sensitive information.
  2. When receiving a result, handle the data carefully and securely.
PublicUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.publicuser;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PublicUserActivity extends Activity {

    private static final int REQUEST_CODE = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    
    public void onUseActivityClick(View view) {
        
        try {
            // *** POINT 4 *** Do not send sensitive information.
            Intent intent = new Intent("org.jssec.android.activity.MY_ACTION");
            intent.putExtra("PARAM", "Not Sensitive Info");
            startActivityForResult(intent, REQUEST_CODE);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                           "Target activity not found.", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // *** POINT 5 *** When receiving a result, handle the data carefully and
        // securely.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        if (resultCode != RESULT_OK) return;
        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");
            Toast.makeText(this,
                           String.format("Received result: \"%s\"", result),
                           Toast.LENGTH_LONG).show();
            break;
        }
    }
}

4.1.1.3. Creating/Using Partner Activities

Partner activities are Activities that can only be used by specific applications. They are used between cooperating partner companies that want to securely share information and functionality.

There is a risk that a third party application can read an Intent that is used to start the Activity. Therefore it is necessary to make sure that if you are putting sensitive information inside an Intent used to start an Activity that you take countermeasures to make sure that it cannot be read by a malicious third party

Sample code for creating a Partner Activity is shown below.

Points (Creating an Activity):

  1. Do not specify taskAffinity.
  2. Do not specify launchMode.
  3. Do not define the intent filter and explicitly set the exported attribute to true.
  4. Verify the requesting application’s certificate through a predefined whitelist.
  5. Handle the received intent carefully and securely, even though the intent was sent from a partner application.
  6. Only return Information that is granted to be disclosed to a partner application.

Please refer to “4.1.3.2. Validating the Requesting Application” for how to validate an application by a white list. Also, please refer to “5.2.1.3. How to Verify the Hash Value of an Application’s Certificate” for how to verify the certificate hash value of a destination application which is specified in the whitelist.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.partneractivity" >
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- Partner activity -->
    <!-- *** POINT 1 *** Do not specify taskAffinity -->
    <!-- *** POINT 2 *** Do not specify launchMode -->
    <!-- *** POINT 3 *** Do not define the intent filter and explicitly set the exported attribute to true -->
    <activity
        android:name=".PartnerActivity"
        android:exported="true" />
    
  </application>
</manifest>
PartnerActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.partneractivity;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PartnerActivity extends Activity {
        
    // *** POINT 4 *** Verify the requesting application's certificate
    // through a predefined whitelist.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();
                
        // Register certificate hash value of partner application
        // org.jssec.android.activity.partneruser.
        sWhitelists.add("org.jssec.android.activity.partneruser", isdebug ?
            // Certificate hash value of "androiddebugkey" in the debug.keystore.
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of "partner key" in the keystore.
            "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");
                
        // Register the other partner applications in the same way.
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
                
        // *** POINT 4 *** Verify the requesting application's certificate
        // through a predefined whitelist.
        if (!checkPartner(this, getCallingActivity().getPackageName())) {
            Toast.makeText(this,
                           "Requesting application is not a partner application.",
                           Toast.LENGTH_LONG).show();
            finish();
            return;
        }
        
        // *** POINT 5 *** Handle the received intent carefully and securely,
        // even though the intent was sent from a partner application.
        // Omitted, since this is a sample. Refer to
        // "3.2 Handling Input Data Carefully and Securely."
        Toast.makeText(this, "Accessed by Partner App", Toast.LENGTH_LONG).show();
    }
    
    public void onReturnResultClick(View view) {

        // *** POINT 6 *** Only return Information that is granted to be disclosed
        // to a partner application.
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Information for partner applications");
        setResult(RESULT_OK, intent);
        finish();
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

Sample code for using a Partner Activity is described below.

Points (Using an Activity):

  1. Verify if the certificate of the target application has been registered in a whitelist.
  2. Do not set the FLAG_ACTIVITY_NEW_TASK flag for the intent that start an activity.
  3. Only send information that is granted to be disclosed to a Partner Activity only by putExtra().
  4. Use explicit intent to call a Partner Activity.
  5. Use startActivityForResult() to call a Partner Activity.
  6. Handle the received result data carefully and securely, even though the data comes from a partner application.

Refer to “4.1.3.2. Validating the Requesting Application” for how to validate applications by white list. Also please refer to “5.2.1.3. How to Verify the Hash Value of an Application’s Certificate” for how to verify the certificate hash value of a destination application which is to be specified in a white list.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.partneruser" >

  <queries>
    <package android:name="org.jssec.android.activity.partneractivity" />
  </queries>
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <activity
        android:name=".PartnerUserActivity"
        android:label="@string/app_name" 
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
PartnerUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.partneruser;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PartnerUserActivity extends Activity {

    // *** POINT 7 *** Verify if the certificate of a target application
    // has been registered in a white list.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();
                
        // Register the certificate hash value of partner application
        // org.jssec.android.activity.partneractivity.
        sWhitelists.add("org.jssec.android.activity.partneractivity", isdebug ?
            // Certificate hash value of "androiddebugkey" is in debug.keystore.
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of "my company key" is in the keystore.
            "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA");
                
        // Register the other partner applications in the same way.
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }
        
    private static final int REQUEST_CODE = 1;

    // Information related the target partner activity
    private static final String TARGET_PACKAGE =
            "org.jssec.android.activity.partneractivity";
    private static final String TARGET_ACTIVITY =
            "org.jssec.android.activity.partneractivity.PartnerActivity";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    
    public void onUseActivityClick(View view) {

        // *** POINT 7 *** Verify if the certificate of the target application
        // has been registered in the own white list.
        if (!checkPartner(this, TARGET_PACKAGE)) {
            Toast.makeText(this,
                           "Target application is not a partner application.",
                           Toast.LENGTH_LONG).show();
            return;
        }
        
        try {
            // *** POINT 8 *** Do not set the FLAG_ACTIVITY_NEW_TASK flag for
            // the intent that start an activity.
            Intent intent = new Intent();
                
            // *** POINT 9 *** Only send information that is granted to be
            // disclosed to a Partner Activity only by putExtra().
            intent.putExtra("PARAM", "Info for Partner Apps");
                
            // *** POINT 10 *** Use explicit intent to call a Partner Activity.
            intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
                
            // *** POINT 11 *** Use startActivityForResult() to call a Partner
            // Activity.
            startActivityForResult(intent, REQUEST_CODE);
        }
        catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                           "Target activity not found.",
                           Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode != RESULT_OK) return;
        
        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");
                        
            // *** POINT 12 *** Handle the received data carefully and securely,
            // even though the data comes from a partner application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            Toast.makeText(this,
                           String.format("Received result: \"%s\"", result),
                           Toast.LENGTH_LONG).show();
            break;
        }
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

4.1.1.4. Creating/Using In-house Activities

In-house activities are the Activities which are prohibited to be used by applications other than other in-house applications. They are used in applications developed internally that want to securely share information and functionality.

There is a risk that a third party application can read an Intent that is used to start the Activity. Therefore it is necessary to make sure that if you are putting sensitive information inside an Intent used to start an Activity that you take countermeasures to make sure that it cannot be read by a malicious third party.

Sample code for creating an In-house Activity is shown below.

Points (Creating an Activity):

  1. Define an in-house signature permission.
  2. Do not specify taskAffinity.
  3. Do not specify launchMode.
  4. Require the in-house signature permission.
  5. Do not define an intent filter and explicitly set the exported attribute to true.
  6. Verify that the in-house signature permission is defined by an in-house application.
  7. Handle the received intent carefully and securely, even though the intent was sent from an in-house application.
  8. Sensitive information can be returned since the requesting application is in-house.
  9. When exporting an APK, sign the APK with the same developer key as the requesting application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.inhouseactivity" >
  
  <!-- *** POINT 1 *** Define an in-house signature permission -->
  <permission
      android:name="org.jssec.android.activity.inhouseactivity.MY_PERMISSION"
      android:protectionLevel="signature" />
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- In-house Activity -->
    <!-- *** POINT 2 *** Do not specify taskAffinity -->
    <!-- *** POINT 3 *** Do not specify launchMode -->
    <!-- *** POINT 4 *** Require the in-house signature permission  -->
    <!-- *** POINT 5 *** Do not define the intent filter and explicitly set the exported attribute to true -->
    <activity
        android:name="org.jssec.android.activity.inhouseactivity.InhouseActivity"
        android:exported="true"
        android:permission="org.jssec.android.activity.inhouseactivity.MY_PERMISSION" />
  </application>
</manifest>
InhouseActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.inhouseactivity;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class InhouseActivity extends Activity {

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.activity.inhouseactivity.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // *** POINT 6 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this,
                           "The in-house signature permission is not declared by in-house application.",
                           Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        // *** POINT 7 *** Handle the received intent carefully and securely,
        // even though the intent was sent from an in-house application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = getIntent().getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Received param: \"%s\"", param),
                       Toast.LENGTH_LONG).show();
    }

    public void onReturnResultClick(View view) {

        // *** POINT 8 *** Sensitive information can be returned since
        // the requesting application is in-house.
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Sensitive Info");
        setResult(RESULT_OK, intent);
        finish();
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point9 *** When exporting an APK, sign the APK with the same developer key as the requesting application.

_images/image35.png

Fig. 4.1.2 Sign the APK with the same developer key as the requesting application

Sample code for using an In-house Activity is described below.

Points (Using an activity):

  1. Declare that you want to use the in-house signature permission.
  2. Verify that the in-house signature permission is defined by an in-house application.
  3. Verify that the destination application is signed with the in-house certificate.
  4. Sensitive information can be sent only by putExtra() since the destination application is in-house.
  5. Use explicit intents to call an In-house Activity.
  6. Handle the received data carefully and securely, even though the data came from an in-house application.
  7. When exporting an APK, sign the APK with the same developer key as the destination application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.inhouseuser" >

  <queries>
    <package android:name="org.jssec.android.activity.inhouseactivity" />
  </queries>
  
  <!-- *** POINT 10 *** Declare to use the in-house signature permission -->
  <uses-permission
      android:name="org.jssec.android.activity.inhouseactivity.MY_PERMISSION" />

  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >

    <activity
        android:name="org.jssec.android.activity.inhouseuser.InhouseUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
InhouseUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.inhouseuser;

import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class InhouseUserActivity extends Activity {

    // Target Activity information
    private static final String TARGET_PACKAGE =
            "org.jssec.android.activity.inhouseactivity";
    private static final String TARGET_ACTIVITY =
            "org.jssec.android.activity.inhouseactivity.InhouseActivity";

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.activity.inhouseactivity.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    private static final int REQUEST_CODE = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public void onUseActivityClick(View view) {

        // *** POINT 11 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this,
                           "The in-house signature permission is not declared by in-house application.",
                           Toast.LENGTH_LONG).show();
            return;
        }

        // ** POINT 12 *** Verify that the destination application is signed
        // with the in-house certificate.
        if (!PkgCert.test(this, TARGET_PACKAGE, myCertHash(this))) {
            Toast.makeText(this,
                           "Target application is not an in-house application.",
                           Toast.LENGTH_LONG).show();
            return;
        }

        try {
            Intent intent = new Intent();

            // *** POINT 13 *** Sensitive information can be sent only by
            // putExtra() since the destination application is in-house.
            intent.putExtra("PARAM", "Sensitive Info");

            // *** POINT 14 *** Use explicit intents to call an In-house Activity.
            intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
            startActivityForResult(intent, REQUEST_CODE);
        }
        catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                           "Target activity not found.",
                           Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode != RESULT_OK) return;

        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");

            // *** POINT 15 *** Handle the received data carefully and securely,
            // even though the data came from an in-house application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            Toast.makeText(this,
                           String.format("Received result: \"%s\"", result),
                           Toast.LENGTH_LONG).show();
            break;
        }
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 16 *** When exporting an APK, sign the APK with the same developer key as the destination application.

_images/image35.png

Fig. 4.1.3 Sign the APK with the same developer key as the destination application

4.1.2. Rule Book

Be sure to follow the rules below when creating or sending an Intent to an activity.

  1. Activities that are Used Only Internally to the Application Must be Set Private (Required)
  2. Do Not Specify taskAffinity (Required)
  3. Do Not Specify launchMode (Required)
  4. Do Not Set the FLAG_ACTIVITY_NEW_TASK Flag for Intents that Start an Activity (Required)
  5. Handling the Received Intent Carefully and Securely (Required)
  6. Use an In-house Defined Signature Permission after Verifying that it is Defined by an In-House Application (Required)
  7. When Returning a Result, Pay Attention to the Possibility of Information Leakage of that Result from the Destination Application (Required)
  8. Use the explicit Intents if the destination Activity is predetermined. (Required)
  9. Handle the Returned Data from a Requested Activity Carefully and Securely (Required)
  10. Verify the Destination Activity if Linking with Another Company’s Application (Required)
  11. When Providing an Asset Secondhand, the Asset should be Protected with the Same Level of Protection (Required)
  12. Sending Sensitive Information Should Be Limited as much as possible (Recommended)

4.1.2.1. Activities that are Used Only Internally to the Application Must be Set Private (Required)

Activities which are only used in a single application are not required to be able to receive any Intents from other applications. Developers often assume that Activities intended to be private will not be attacked but it is necessary to explicitly make these Activities private in order to stop malicious Intents from being received.

AndroidManifest.xml
        <!-- Private activity -->
        <!-- *** 4.1.1.1 - POINT 3 *** Explicitly set the exported attribute to false. -->
        <activity
            android:name=".PrivateActivity"
            android:label="@string/app_name"
            android:exported="false" />

Intent filters should not be set on activities that are only used in a single application. Due to the characteristics of Intent filters, Due to the characteristics of how Intent filters work, even if you intend to send an Intent to a Private Activity internally, if you send the Intent through an Intent filter than you may unintentionally start another Activity. Please see Advanced Topics “4.1.3.1. Combination of Exported Attribute and Intent Filter Setting (For Activity)” for more details.

AndroidManifest.xml(Not recommended)
       <!-- Private activity -->
       <!-- *** 4.1.1.1 - POINT 3 *** Explicitly set the exported attribute to false. -->
       <activity
           android:name=".PictureActivity"
           android:label="@string/picture_name"
           android:exported="false" >
           <intent-filter>
               <action android:name=”org.jssec.android.activity.OPEN />
           </intent-filter>
       </activity>

4.1.2.2. Do Not Specify taskAffinity (Required)

In Android OS, Activities are managed by tasks. Task names are determined by the affinity that the root Activity has. On the other hand, for Activities other than root Activities, the task to which the Activity belongs is not determined by the Affinity only, but also depends on the Activity’s launch mode. Please refer to “4.1.3.4. Root Activity” for more details.

In the default setting, each Activity uses its package name as its affinity. As a result, tasks are allocated according to application, so all Activities in a single application will belong to the same task. To change the task allocation, you can make an explicit declaration for the affinity in the AndroidManifest.xml file or you can set a flag in an Intent sent to an Activity. However, if you change task allocations, there is a risk that another application could read the Intents sent to Activities belonging to another task.

Be sure not to specify android:taskAffinity in the AndroidManifest.xml file and use the default setting keeping the affinity as the package name in order to prevent sensitive information inside sent or received Intents from being read by another application.

Below is an example AndroidManifest.xml file for creating and using Private Activities.

AndroidManifest.xml
    <!-- *** 4.1.1.1 - POINT 1 *** Do not specify taskAffinity -->
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >

        <!-- *** 4.1.1.1 - POINT 1 *** Do not specify taskAffinity -->
        <activity
            android:name=".PrivateUserActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Private activity -->
        <!-- *** 4.1.1.1 - POINT 1 *** Do not specify taskAffinity -->
        <activity
            android:name=".PrivateActivity"
            android:label="@string/app_name"
            android:exported="false" />
     </application>

Please refer to the “Google Android Programming guide”[2], the Google Developer’s API Guide “Tasks and Back Stack”[3], “4.1.3.3. Reading Intents Sent to an Activity” and “4.1.3.4. Root Activity” for more details about tasks and affinities.

[2]Author Egawa, Fujii, Asano, Fujita, Yamada, Yamaoka, Sano, Takebata, “Google Android Programming Guide”, ASCII Media Works, July 2009
[3]https://developer.android.com/guide/components/tasks-and-back-stack.html

4.1.2.3. Do Not Specify launchMode (Required)

The Activity launch mode is used to control the settings for creating new tasks and Activity instances when starting an Activity. By default it is set to “standard”. In the “standard” setting, new instances are always created when starting an Activity, tasks follow the tasks belonging to the calling Activity, and it is not possible to create a new task. When a new task is created, it is possible for other applications to read the contents of the calling Intent so it is required to use the “standard” Activity launch mode setting when sensitive information is included in an Intent.

The Activity launch mode can be explicitly set in the android:launchMode attribute in the AndroidManifest.xml file, but because of the reason explained above, this should not be set in the Activity declaration and the value should be kept as the default “standard”.

AndroidManifest.xml
        <!-- *** 4.1.1.1 - POINT 2 *** Do not specify launchMode -->
        <activity
            android:name=".PrivateUserActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Private activity -->
        <!-- *** 4.1.1.1 - POINT 2 *** Do not specify launchMode -->
        <activity
            android:name=".PrivateActivity"
            android:label="@string/app_name"
            android:exported="false" />
    </application>

Please refer to “4.1.3.3. Reading Intents Sent to an Activity” and “4.1.3.4. Root Activity.”

4.1.2.4. Do Not Set the FLAG_ACTIVITY_NEW_TASK Flag for Intents that Start an Activity (Required)

The launch mode of an Activity can be changed when executing startActivity() or startActivityForResult() and in some cases a new task may be generated. Therefore it is necessary to not change the launch mode of Activity during execution.

To change the Activity launch mode, set the Intent flags by using setFlags() or addFlags() and use that Intent as an argument to startActivity() or startActivityForResult(). FLAG_ACTIVITY_NEW_TASK is the flag used to create a new task. When the FLAG_ACTIVITY_NEW_TASK is set, a new task will be created if the called Activity does not exist in the background or foreground.

The FLAG_ACTIVITY_MULTIPLE_TASK flag can be set simultaneously with FLAG_ACTIVITY_NEW_TASK. In this case, a new task will always be created. New tasks may be created with either setting so these should not be set with Intents that handle sensitive information.

Example of sending an intent

        Intent intent = new Intent();

        // *** 4.1.1.1 - POINT 6 *** Do not set the FLAG_ACTIVITY_NEW_TASK flag
        // for the intent to start an activity.

        intent.setClass(this, PrivateActivity.class);
        intent.putExtra("PARAM", "Sensitive Info");

        startActivityForResult(intent, REQUEST_CODE);

In addition, you may think that there is a way to prevent the contents of an Intent from being read even if a new task was created by explicitly setting the FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS flag. However, even by using this method, the contents can be read by a third party so you should avoid any usage of FLAG_ACTIVITY_NEW_TASK.

Please refer to “4.1.3.1. Combination of Exported Attribute and Intent Filter Setting (For Activity)”, “4.1.3.3. Reading Intents Sent to an Activity” and “4.1.3.4. Root Activity”.

4.1.2.5. Handling the Received Intent Carefully and Securely (Required)

Risks differ depending on the types of Activity, but when processing a received Intent data, the first thing you should do is input validation.

Since Public Activities can receive Intents from untrusted sources, they can be attacked by malware. On the other hand, Private Activities will never receive any Intents from other applications directly, but it is possible that a Public Activity in the targeted application may forward a malicious Intent to a Private Activity so you should not assume that Private Activities cannot receive any malicious input. Since Partner Activities and In-house Activities also have the risk of a malicious intent being forwarded to them as well, it is necessary to perform input validation on these Intents as well.

Please refer to “3.2. Handling Input Data Carefully and Securely”.

4.1.2.6. Use an In-house Defined Signature Permission after Verifying that it is Defined by an In-House Application (Required)

Make sure to protect your in-house Activities by defining an in-house signature permission when creating the Activity. Since defining a permission in the AndroidManifest.xml file or declaring a permission request does not provide adequate security, please be sure to refer to “5.2.1.2. How to Communicate Between In-house Applications with In-house-defined Signature Permission.”

4.1.2.7. When Returning a Result, Pay Attention to the Possibility of Information Leakage of that Result from the Destination Application (Required)

When you use setResult() to return data, the reliability of the destination application will depend on the Activity type. When Public Activities are used to return data, the destination may turn out to be malware in which case that information could be used in a malicious way. For Private and In-house Activities, there is not much need to worry about data being returned to be used maliciously because they are being returned to an application you control. Partner Activities are somewhat in the middle.

As above, when returning data from Activities, you need to pay attention to information leakage from the destination application.

Example of returning data.

    public void onReturnResultClick(View view) {

        // *** 4.1.1.1 - POINT 6 *** Information that is granted to be disclosed
        // to a partner application can be returned.
        Intent intent = new Intent();
        intent.putExtra("RESULT",
            "Information that is granted to disclose to partner applications");
        setResult(RESULT_OK, intent);
        finish();
    }

4.1.2.8. Use the explicit Intents if the destination Activity is predetermined. (Required)

When using an Activity by implicit Intents, the Activity in which the Intent gets sent to is determined by the Android OS. If the Intent is mistakenly sent to malware then Information leakage can occur. On the other hand, when using an Activity by explicit Intents, only the intended Activity will receive the Intent so this is much safer.

Unless it is absolutely necessary for the user to determine which application’s Activity the intent should be sent to, you should use explicit intents and specify the destination in advance.

Using an Activity in the same application by an explicit Intent

        Intent intent = new Intent(this, PictureActivity.class);
        intent.putExtra("BARCODE", barcode);
        startActivity(intent);

Using other applicaion’s Public Activity by an explicit Intent

        Intent intent = new Intent();
        intent.setClassName(
            "org.jssec.android.activity.publicactivity",
            "org.jssec.android.activity.publicactivity.PublicActivity");
        startActivity(intent);

However, even when using another application’s Public Activity by explicit Intents, it is possible that the destination Activity could be malware. This is because even if you limit the destination by package name, it is still possible that a malicious application can fake the same package name as the real application. To eliminate this type of risk, it is necessary to consider using a Partner or In-house.

Please refer to “4.1.3.1. Combination of Exported Attribute and Intent Filter Setting (For Activity)”.

4.1.2.9. Handle the Returned Data from a Requested Activity Carefully and Securely (Required)

While the risks differ slightly according to what type of Activity you accessing, when processing Intent data received as a returned value, you always need to perform input validation on the received data.

Public Activities have to accept returned Intents from untrusted sources so when accessing a Public Activity it is possible that, the returned Intents are actually sent by malware. It is often mistakenly thought that all returned Intents from a Private Activity are safe because they are originating from the same application. However, since it is possible that an intent received from an untrusted source is indirectly forwarded, you should not blindly trust the contents of that Intent. Partner and In-house Activities have a risk somewhat in the middle of Private and Public Activities. Be sure to input validate these Activities as well.

Please refer to “3.2. Handling Input Data Carefully and Securely” for more information.

4.1.2.10. Verify the Destination Activity if Linking with Another Company’s Application (Required)

Be sure to sure a whitelist when linking with another company’s application. You can do this by saving a copy of the company’s certificate hash inside your application and checking it with the certificate hash of the destination application. This will prevent a malicious application from being able to spoof Intents. Please refer to sample code section “4.1.1.3. Creating/Using Partner Activities” for the concrete implementation method. For technical details, please refer to “4.1.3.2. Validating the Requesting Application.”

4.1.2.11. When Providing an Asset Secondhand, the Asset should be Protected with the Same Level of Protection (Required)

When an information or function asset, which is protected by a permission, is provided to another application secondhand, you need to make sure that it has the same required permissions needed to access the asset. In the Android OS permission security model, only an application that has been granted proper permissions can directly access a protected asset. However, there is a loophole because an application with permissions to an asset can act as a proxy and allow access to an unprivileged application. Substantially this is the same as re-delegating a permission so it is referred to as the “Permission Re-delegation” problem. Please refer to “5.2.3.4. Permission Re-delegation Problem.”

4.1.2.12. Sending Sensitive Information Should Be Limited as much as possible (Recommended)

You should not send sensitive information to untrusted parties. Even when you are linking with a specific application, there is still a chance that you unintentionally send an Intent to a different application or that a malicious third party can steal your Intents. Please refer to “4.1.3.5. Log Output When using Activities.”

You need to consider the risk of information leakage when sending sensitive information to an Activity. You must assume that all data in Intents sent to a Public Activity can be obtained by a malicious third party. In addition, there is a variety of risks of information leakage when sending Intents to Partner or In-house Activities as well depending on the implementation. Even when sending data to Private Activities, there is a risk that the data in the Intent could be leaked through LogCat. Information in the extras part of the Intent is not output to LogCat so it is best to store sensitive information there.

However, not sending sensitive data in the first place is the only perfect solution to prevent information leakage therefore you should limit the amount of sensitive information being sent as much as possible. When it is necessary to send sensitive information, the best practice is to only send to a trusted Activity and to make sure the information cannot be leaked through LogCat.

In addition, sensitive information should never be sent to the root Activity. Root Activities are Activities that are called first when a task is created. For example, the Activity which is launched from launcher is always the root Activity.

Please refer to “4.1.3.3. Reading Intents Sent to an Activity” and “4.1.3.4. Root Activity” for more details on root Activities.

4.1.3. Advanced Topics

4.1.3.1. Combination of Exported Attribute and Intent Filter Setting (For Activity)

We have explained how to implement the four types of Activities in this guidebook: Private Activities, Public Activities, Partner Activities, and In-house Activities. The various combinations of permitted settings for each type of exported attribute defined in the AndroidManifest.xml file and the intent-filter elements are defined in the table below. Please verify the compatibility of the exported attribute and intent-filter element with the Activity you are trying to create.

Table 4.1.2 Combination of Exported Attribute and intent-filter Element
  Value of exported attribute
true false Not specified
Intent Filter defined Public (Do not Use) (Do not Use)
Intent Filter Not Defined Public, Partner,In-house Private (Do not Use)

When the exported attribute of an Activity is left unspecified, the question of whether or not the Activity is public is determined by the presence or absence of Intent filters for that Activity [4]. However, in this guidebook it is forbidden to set the exported attribute to unspecified. In general, as mentioned previously, it is best to avoid implementations that rely on the default behavior of any given API; moreover, in cases where explicit methods exist for configuring important security-related settings such as the exported attribute, it is always a good idea to make use of those methods.

[4]If any Intent filters are defined, the Activity is public; otherwise it is private. Refer https://developer.android.com/guide/topics/manifest/activity-element.html#exported

The reason why “an undefined Intent filter and an exported attribute of false” should not be used is that there is a loophole in Android’s behavior, and because of how Intent filters work, other application’s Activities can be called unexpectedly. The following two figures below show this explanation. Fig. 4.1.4 is an example of normal behavior in which a Private Activity (application A) can be called by an implicit Intent only from the same application. The Intent filter (action=”X” in the figure) is defined only in application A, so this is the expected behavior.

_images/image36.png

Fig. 4.1.4 An Example of Normal Behavior

Fig. 4.1.5 below shows a scenario in which the same Intent filter (action=”X”) is defined in Application B as well as Application A. In Fig. 4.1.5, Application A is trying to call a Private Activity in the same application by sending an implicit Intent, but this time, a dialog box asking the user which application to select is displayed, and the Public Activity B-1 in Application B is called by mistake due to the user selection [5]. Due to this loophole, it is possible that sensitive information can be sent to other applications or the application may receive an unexpected return value.

_images/image37.png

Fig. 4.1.5 An Example of Abnormal Behavior

As shown above, using Intent filters to send implicit Intents to Private Activities may result in unexpected behavior so it is best to avoid this setting. In addition, we have verified that this behavior does not depend on the installation order of Application A and Application B.

[5]For terminals running Android 8.0 (API Level 26) or later, it

has been confirmed that the “Complete action using” dialog is not displayed and an automatic transition is made to the Public Activity B-1 in Application B in the figure. For this reason, it should be prohibited to start a Private Activity with Intent filters by an implicit Intent.

Furthermore, if Android 12 is the target, and the application contains Activity, Service, or Broadcast Receiver that use Intent filters, the explicit declaration of the exported attribute is required. If omitted, the build itself will become invalid.

In this case, a warning is displayed on the manifest file and an error message is displayed during the build [6].

  • Warning message of the manifest file

When using intent filters, please specify android:exported as well

  • Error message during build

Manifest merger failed : android:exported needs to be explicitly specified for <activity>. Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.`

[6]Confirmed on Android Studio Arctic Fox 2020.3.1

4.1.3.2. Validating the Requesting Application

Here we explain the technical information about how to implement a Partner Activity. Partner applications permit that only particular applications which are registered in a whitelist are allowed access and all other applications are denied. Because applications other than in-house applications also need access permission, we cannot use signature permissions for access control.

Simply speaking, we want to validate the application trying to use the Partner Activity by checking if it is registered in a predefined whitelist and allow access if it is and deny access if it is not. Application validation is done by obtaining the certificate from the application requesting access and comparing its hash with the one in the whitelist.

Some developers may think that it is sufficient to just compare “the package name” without obtaining “the certificate”, however, it is easy to spoof the package name of a legitimate application so this is not a good method to check for authenticity. Arbitrarily assignable values should not be used for authentication. On the other hand, because only the application developer has the developer key for signing its certificate, this is a better method for identification. Since the certificate cannot be easily spoofed, unless a malicious third party can steal the developer key, there is a very small chance that malicious application will be trusted. While it is possible to store the entire certificate in the whitelist, it is sufficient to only store the SHA-256 hash value in order to minimize the file size.

There are two restrictions for using this method.

  • The requesting application has to use startActivityForResult() instead of startActivity().
  • The requesting application can only call from an Activity.

The second restriction is the restriction imposed as a result of the first restriction, so technically there is only a single restriction.

This restriction occurs due to the restriction of Activity.getCallingPackage() which gets the package name of the calling application. Activity.getCallingPackage() returns the package name of source (requesting) application only in case it is called by startActivityForResult(), but unfortunately, when it is called by startActivity(), it only returns null. Because of this, when using the method explained here, the source (requesting) application needs to use startActivityForResult() even if it does not need to obtain a return value. In addition, startActivityForResult() can be used only in Activity classes, so the source (requester) is limited to Activities.

PartnerActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.partneractivity;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PartnerActivity extends Activity {
        
    // *** POINT 4 *** Verify the requesting application's certificate
    // through a predefined whitelist.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();
                
        // Register certificate hash value of partner application
        // org.jssec.android.activity.partneruser.
        sWhitelists.add("org.jssec.android.activity.partneruser", isdebug ?
            // Certificate hash value of "androiddebugkey" in the debug.keystore.
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of "partner key" in the keystore.
            "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");
                
        // Register the other partner applications in the same way.
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
                
        // *** POINT 4 *** Verify the requesting application's certificate
        // through a predefined whitelist.
        if (!checkPartner(this, getCallingActivity().getPackageName())) {
            Toast.makeText(this,
                           "Requesting application is not a partner application.",
                           Toast.LENGTH_LONG).show();
            finish();
            return;
        }
        
        // *** POINT 5 *** Handle the received intent carefully and securely,
        // even though the intent was sent from a partner application.
        // Omitted, since this is a sample. Refer to
        // "3.2 Handling Input Data Carefully and Securely."
        Toast.makeText(this, "Accessed by Partner App", Toast.LENGTH_LONG).show();
    }
    
    public void onReturnResultClick(View view) {

        // *** POINT 6 *** Only return Information that is granted to be disclosed
        // to a partner application.
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Information for partner applications");
        setResult(RESULT_OK, intent);
        finish();
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

4.1.3.3. Reading Intents Sent to an Activity

In Android 5.0 (API Level 21) and later, the information retrieved with getRecentTasks() has been limited to the caller’s own tasks and possibly some other tasks such as home that are known to not be sensitive. However applications, which support the versions under Android 5.0 (API Level 21), should protect against leaking sensitive information.

The following describes the contents of this problem occurring in Android 5.0 and earlier version.

Intents that are sent to the task’s root Activity are added to the task history. A root Activity is the first Activity started in a task. It is possible for any application to read the Intents added to the task history by using the ActivityManager class.

Sample code for reading the task history from an application is shown below. To browse the task history, specify the GET_TASKS permission in the AndroidManifest.xml file.

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.intent.maliciousactivity" >

  <!-- Use GET_TASKS Permission -->
  <uses-permission android:name="android.permission.GET_TASKS" />

  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:theme="@style/AppTheme" >
    <activity
        android:name=".MaliciousActivity"
        android:label="@string/title_activity_main"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
MaliciousActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.intent.maliciousactivity;

import java.util.List;
import java.util.Set;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class MaliciousActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.malicious_activity);

        // Get am ActivityManager instance.
        ActivityManager activityManager =
                (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        // Get 100 recent task info.
        List<ActivityManager.RecentTaskInfo> list = activityManager
            .getRecentTasks(100, ActivityManager.RECENT_WITH_EXCLUDED);
        for (ActivityManager.RecentTaskInfo r : list) {
            // Get Intent sent to root Activity and Log it.
            Intent intent = r.baseIntent;
            Log.v("baseIntent", intent.toString());
            Log.v("  action:", intent.getAction());
            String target = intent.getDataString();
            if (target != null) {
                Log.v("  data:", intent.getDataString());
            }
            if (r.origActivity != null) {
                Log.v("  pkg:", r.origActivity.getPackageName() +
                                r.origActivity.getClassName());
            }
            Bundle extras = intent.getExtras();
            if (extras != null) {
                Set<String> keys = extras.keySet();
                for(String key : keys) {
                    Log.v("  extras:", key + "=" + extras.get(key).toString());
                }
            }
        }
    }
}

You can obtain specified entries of the task history by using the getRecentTasks() function of the AcitivityManager class. Information about each task is stored in an instance of the ActivityManager.RecentTaskInfo class, but Intents that were sent to the task’s root Activity are stored in its member variable baseIntent. Since the root Activity is the Activity which was started when the task was created, please be sure to not fulfill the following two conditions when calling an Activity.

  • A new task is created when the Activity is called.
  • The called Activity is the task’s root Activity which already exists in the background or foreground.

4.1.3.4. Root Activity

The root Activity is the Activity which is the starting point of a task. In other words, this is the Activity which was launched when task was created. For example, when the default Activity is launched by launcher, this Activity will be the root Activity. According to the Android specifications, the contents of Intents sent to the root Activity can be read from arbitrary applications. So, it is necessary to take countermeasures not to send sensitive information to the root Activity. In this guidebook, the following three rules have been made to avoid a called Activity to become root Activity.

  • taskAffinity should not be specified.
  • launchMode should not be specified.
  • The FLAG_ACTIVITY_NEW_TASK flag should not be set in an Intent sent to an Activity.

We consider the situations that an Activity can become the root Activity below. A called Activity becoming a root Activity depends on the following.

  • The launch mode of the called Activity
  • The task of a called Activity and its launch mode

First of all, let me explain “the Launch mode of called Activity”. Launch mode of Activity can be set by writing android:launchMode in AndroidManifest.xml. When it’s not written, it’s considered as “standard”. In addition, launch mode can be also changed by a flag to set to Intent. Flag “FLAG_ACTIVITY_NEW_TASK” launches Activity by “singleTask” mode.

The launch modes that can be specified are as per below. I’ll explain about the relation with the root activity, mainly.

standard

Activity which is called by this mode won’t be root, and it belongs to the caller side task. Every time it’s called, Instance of Activity is to be generated.

singleTop

This launch mode is the same as “standard”, except for that the instance is not generated when launching an Activity which is displayed in most front side of foreground task.

singleTask

This launch mode determines the task to which the activity would be belonging by Affinity value. When task which is matched with Activity’s affinity doesn’t exist either in background or in foreground, a new task is generated along with Activity’s instance. When task exists, neither of them is to be generated. In the former one, the launched Activity’s Instance becomes root.

singleInstance

Same as “singleTask”, but following point is different. Only root Activity can belongs to the newly generated task. So instance of Activity which was launched by this mode is always root activity. Now, we need to pay attention to the case that the class name of called Activity and the class name of Activity which is included in a task are different although the task which has the same name of called Activity’s affinity already exists.

From as above, we can get to know that Activity which was launched by “singleTask” or “singleInstance” has the possibility to become root. In order to secure the application’s safety, it should not be launched by these modes.

Next, I’ll explain about “Task of the called Activity and its launch mode”. Even if Activity is called by “standard” mode, it becomes root Activity in some cases depends on the task state to which Activity belongs.

For example, think about the case that called Activity’s task has being run already in background.

The problem here is the case that Activity Instance of the task is launched by “singleInstance”. When the affinity of Activity which was called by “standard” is same with the task, new task is to be generated by the restriction of existing “singleInstance” Activity. However, when class name of each Activity is same, task is not generated and existing activity Instance is to be used. In any cases, that called Activity becomes root Activity.

As per above, the conditions that root Activity is called are complicated, for example it depends on the state of execution. So when developing applications, it’s better to contrive that Activity is called by “standard”.

As an example of that Intent which is sent to Private Activity is read out form other application, the sample code shows the case that caller side Activity of private Activity is launched by “singleInstance” mode. In this sample code, private activity is launched by “standard” mode, but this private Activity becomes root Activity of new task due the “singleInstance” condition of caller side Activity. At this moment, sensitive information that is sent to Private Activity is recorded task history, so it can be read out from other applications. FYI, both caller side Activity and Private Activity have the same affinity.

AndroidManifest.xml(Not recommended)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.activity.singleinstanceactivity" >
  
  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- Set the launchMode of the root Activity to "singleInstance". -->
    <!-- Do not use taskAffinity -->
    <activity
        android:name="org.jssec.android.activity.singleinstanceactivity.PrivateUserActivity"
        android:label="@string/app_name"
        android:launchMode="singleInstance"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    
    <!-- Private activity -->
    <!-- Set the launchMode to "standard." -->
    <!-- Do not use taskAffinity -->
    <activity
        android:name="org.jssec.android.activity.singleinstanceactivity.PrivateActivity"
        android:label="@string/app_name"
        android:exported="false" />
  </application>
</manifest>

Private Activity only returns the results to the received Intent.

PrivateActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.singleinstanceactivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PrivateActivity extends Activity {
        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.private_activity);

        // Handle intent securely, even though the intent sent from
        // the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = getIntent().getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Received param: \"%s\"", param),
                       Toast.LENGTH_LONG).show();
    }

    public void onReturnResultClick(View view) {        
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Sensitive Info");
        setResult(RESULT_OK, intent);
        finish();
    }
}

In caller side of Private Activity, Private Activity is launched by “standard” mode without setting flag to Intent.

PrivateUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.activity.singleinstanceactivity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class PrivateUserActivity extends Activity {

    private static final int REQUEST_CODE = 1;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
    }
    
    public void onUseActivityClick(View view) {
        
        // Start the Private Activity with "standard" lanchMode.
        Intent intent = new Intent(this, PrivateActivity.class);
        intent.putExtra("PARAM", "Sensitive Info");
        
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode != RESULT_OK) return;
        
        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");
                        
            // Handle received result data carefully and securely,
            // even though the data came from the Activity in the same application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            Toast.makeText(this,
                           String.format("Received result: \"%s\"", result),
                           Toast.LENGTH_LONG).show();
            break;
        }
    }
}

4.1.3.5. Log Output When using Activities

When using an activity, the contents of intent are output to LogCat by ActivityManager. The following contents are to be output to LogCat, so in this case, sensitive information should not be included here.

  • Destination Package name
  • Destination Class name
  • URI which is set by Intent#setData()

For example, when an application sent mails, the mail address is unfortunately outputted to LogCat if the application would specify the mail address to URI. So, better to send by setting Extras.

When sending a mail as below, mail address is shown to the logCat.

MainActivity.java
    // URI is output to the LogCat.
    Uri uri = Uri.parse("mailto:test@gmail.com");
    Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
    startActivity(intent);

When using Extras, mail address is no more shown to the logCat.

MainActivity.java
    // Contents which was set to Extra, is not output to the LogCat.
    Uri uri = Uri.parse("mailto:");
    Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
    intent.putExtra(Intent.EXTRA_EMAIL, new String[] {"test@gmail.com"});
    startActivity(intent);

However, there are cases where other applications can read the Extras data of intent using ActivityManager#getRecentTasks(). Please refer to “4.1.2.2. Do Not Specify taskAffinity (Required)”, “4.1.2.3. Do Not Specify launchMode (Required)” and “4.1.2.4. Do Not Set the FLAG_ACTIVITY_NEW_TASK Flag for Intents that Start an Activity (Required)”.

4.1.3.6. Protecting against Fragment Injection in PreferenceActivity

When a class derived from PreferenceActivity is a public Activity, a problem known as Fragment Injection [7] may arise. To prevent this problem from arising, it is necessary to override PreferenceActivity.IsValidFragment() and check the validity of its arguments to ensure that the Activity does not handle any Fragments without intention. (For more on the safety of input data, see Section “3.2. Handling Input Data Carefully and Securely”.)

[7]For more information on Fragment Injection, consult this URL: https://securityintelligence.com/new-vulnerability-android-framework-fragment-injection/

Below we show a sample in which IsValidFragment() has been overridden. Note that, if the source code has been obfuscated, class names and the results of parameter-value comparisons may change. In this case it is necessary to pursue alternative countermeasures.

Example of an overridden isValidFragment() method

    protected boolean isValidFragment(String fragmentName) {
        // If the source code is obfuscated, we must pursue alternative strategies
        return PreferenceFragmentA.class.getName().equals(fragmentName)
                || PreferenceFragmentB.class.getName().equals(fragmentName)
                || PreferenceFragmentC.class.getName().equals(fragmentName)
                || PreferenceFragmentD.class.getName().equals(fragmentName);
    }

Note that if the app’s targetSdkVersion is 19 or greater, failure to override PreferenceActivity.isValidFragment() will result in a security exception and the termination of the app whenever a Fragment is inserted [when isValidFragment() is called], so in this case overriding PreferenceActivity.isValidFragment() is mandatory.

4.1.3.7. The Autofill framework

The Autofill framework was added in Android 8.0 (API Level 26). Using this framework allows apps to store information entered by users—such as user names, passwords, addresses, phone numbers, and credit cards—and subsequently to retrieve this information as necessary to allow the app to fill in forms automatically. This is a convenient mechanism that reduces data-entry burdens for users; however, because it allows a given app to pass sensitive information such as passwords and credit cards to other apps, it must be handled with appropriate care.

Overview of the framework
2 components

In what follows, we provide an overview of the two components [8] registered by the Autofill framework.

  • Apps eligible for Autofill (user apps):
    • Pass view information (text and attributes) to Autofill service; receive information from Autofill service as needed to auto-fill forms.
    • All apps that have Activities are user apps (when in the foreground).
    • It is possible for all Views of all user apps to be eligible for Autofill. It is also possible to explicitly specify that any given individual view should be ineligible for Autofill.
    • It is also possible to restrict an app’s use of Autofill to the Autofill service within the same package.
  • Services that provide Autofill (Autofill services):
    • Save View information passed by an app (requires user permission); provide an app with information needed for Autofill in a View (candidate lists).
    • The Views eligible for this information saving are determined by the Autofill service. (Within the Autofill framework, by default information on all Views contained in an Activity are passed to the Autofill service.)
    • It is also possible to construct Autofill services provided by third parties.
    • It is possible for several to be present within a single terminal with only the service selected by the user via “Settings” enabled (“None” is also a possible selection.)
    • It also possible for a Service to provide a UI to validate users via password entry or other mechanisms to protect the security of the user information handled.
[8]The “user app” and the “Autofill service” may belong to the same package (the same APK file) or to different packages.
Procedural flowchart for the Autofill framework

Fig. 4.1.6 is a flowchart illustrating the procedural flow of interactions among Autofill-related components during Autofill. When triggered by events such as motion of the focus in a user app’s View, information on that View (primarily the parent-child relationships and various attributes of the View) is passed via the Autofill framework to the Autofill service selected within “Settings”. Based on the data it receives, the Autofill service fetches from a database the information (candidate lists) needed for Autofill, then returns this to the framework. The framework displays a candidate list to the user, and the app carries out the Autofill operation using the data selected by the user.

_images/image38.png

Fig. 4.1.6 Procedural flow among components for Autofill

Next, Fig. 4.1.7 is a flowchart illustrating the procedural flow for saving user data via Autofill. Upon a triggering event such as when AutofillManager#commit() is called or when an Activity is unfocused, if any Autofilled values for the View have been modified and the user has granted permission via the Save Permission dialog box displayed by the Autofill framework, information on the View (including text) is passed via the Autofill framework to the Autofill service selected via “Settings”, and the Autofill service stores information in the database to complete the procedural sequence.

_images/image39.png

Fig. 4.1.7 Procedural flow among components for saving user data

Security concerns for Autofill user apps

As noted in the section “Overview of the framework” above, the security model adopted by the Autofill framework is premised on the assumption that the user configures the “Settings” to select secure Autofill services and makes appropriate decisions regarding which data to pass to which Autofill service when storing data.

However, if a user unwittingly selects a non-secure Autofill service, there is a possibility that the user may permit the storage of sensitive information that should not be passed to the Autofill service. In what follows we discuss the damage that could result in such a scenario.

When saving information, if the user selects an Autofill service and grants it permission via the Save Permission dialog box, information for all Views contained in the Activity currently displayed by the app in use may be passed to the Autofill service. If the Autofill service is malware, or if other security issues arise—for example, if View information is stored by the Autofill service on an external storage medium or on an insecure cloud service—this could create the risk that information handled by the app might be leaked.

On the other hand, during Autofill, if the user has selected a piece of malware as the Autofill service, values transmitted by the malware may be entered as input. At this point, if the security of the data input is not adequately validated by the app or by the cloud services to which the app sends data, risks of information leakage and/or termination of the app or the service may arise.

Note that, as discussed above in the section “2 components”, apps with Activities are automatically eligible for Autofill, and thus all developers of apps with Activities must take the risks described above into account when designing and implementing apps. In what follows we will present countermeasures to mitigate the risks described above we recommend that these be adopted as appropriate based on a consideration of the countermeasures required by an app—referring to “3.1.3. Asset Classification and Protective Countermeasures” and other relevant resources.

Steps to mitigate risk: 1

As discussed above, security within the Autofill framework is ultimately guaranteed only at the user’s discretion. For this reason, the range of countermeasures available to apps is somewhat limited. However, there is one way to mitigate the concerns described above: Setting the importantForAutofill attribute for a view to “no” ensures that no View information is passed to the Autofill service (i.e. the View is made ineligible for Autofill), even if the user cannot make appropriate selections or permissions (such as selecting a piece of malware as the Autofill service) [9].

[9]Even after taking this step, in some cases it may not be possible to avoid the security concerns described above—for example, if the user intentionally uses Autofill. Implementing the steps described in “Steps to mitigate risk: 2” will improve security in these cases.

The importantForAutofill attribute may be specified by any of the following methods.

  • Set the importantForAutofill attribute in the layout XML
  • Call View#setImportantForAutofill()

The values that may be set for this attribute are shown below. Make sure to use values appropriate for the specified range. In particular, note with caution that, when a value is set to “no” for a View, that View will be ineligible for Autofill, but its children will remain eligible for Autofill. The default value is “auto.”

Table 4.1.3 Eligible for Autofill?

Value

Name of constant
Specified View Child View

“auto”

IMPORTANT_FOR_AUTOFILL_AUTO
“auto” [10] “auto” [10]

“no”

IMPORTANT_FOR_AUTOFILL_NO
No Yes

“noExcludeDescendants”

IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
No No

“yes”

IMPORTANT_FOR_AUTOFILL_YES
Yes Yes

“yesExcludeDescendants”

IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS
Yes No
[10](1, 2) Determined by Autofill framework

It is also possible to use AutofillManager#hasEnabledAutofillServices() to restrict the use of Autofill functionality to Autofill services within the same package.

In what follows, we show an example that all Views in an Activity are eligible for Autofill (whether or not a View actually uses Autofill is determined by the Autofill service) only in case that “Settings” have been configured to use a Autofill service within the same package. It is also possible to call View#setImportantForAutofill() for individual Views.

DisableForOtherServiceActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.autofillframework.autofillapp;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.TextView;

import org.jssec.android.autofillframework.R;

public class DisableForOtherServiceActivity extends AppCompatActivity {
    private boolean mIsAutofillEnabled = false;

    private EditText mUsernameEditText;
    private EditText mPasswordEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.disable_for_other_service_activity);

        mUsernameEditText = (EditText)findViewById(R.id.field_username);
        mPasswordEditText = (EditText)findViewById(R.id.field_password);

        findViewById(R.id.button_login).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
            }
        });

        findViewById(R.id.button_clear).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                resetFields();
            }
        });

        //Because the floating-toolbar is not supported for this Activity,
        // Autofill may be used by selecting "Automatic Input"
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateAutofillStatus();
        View rootView = this.getWindow().getDecorView();
        if (!mIsAutofillEnabled) {
            //If not using Autofill service within the same package,
            // make all Views ineligible for Autofill
            rootView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
        } else {
            //If using Autofill service within the same package,
            // make all Views eligible for Autofill
            //View#setImportantForAutofill() may also be called for specific Views
            rootView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_AUTO);
        }
    }

    private void login() {
        String username = mUsernameEditText.getText().toString();
        String password = mPasswordEditText.getText().toString();

        //Validate data obtained from View
        if (!Util.validateUsername(username) || !Util.validatePassword(password)) {
            //appropriate error handling
        }

        //Send username, password to server

        finish();
    }

    private void resetFields() {
        mUsernameEditText.setText("");
        mPasswordEditText.setText("");
    }

    private void updateAutofillStatus() {
        AutofillManager mgr = getSystemService(AutofillManager.class);

        mIsAutofillEnabled = mgr.hasEnabledAutofillServices();

        TextView statusView = (TextView) findViewById(R.id.label_autofill_status);
        String status = "Our autofill service is --.";
        if (mIsAutofillEnabled) {
            status = "autofill service within same package is enabled";
        } else {
            status = "autofill service within same package is disabled";
        }
        statusView.setText(status);
    }
}
Steps to mitigate risk: 2

Even in cases where an app has implemented the steps described in the previous section (”Steps to mitigate risk: 1”), the user can forcibly enable the use of Autofill by long-pressing the View, displaying the floating toolbar or a similar control interface, and selecting “Automatic input”. In this case, information for all Views—including Views for which the importantForAutofill attribute has been set to “no,” or for which similar steps have been taken—will be passed to the Autofill service.

It is possible to avoid the risk of information leakage even in circumstances such as these by deleting the “Automatic Input” option from the floating-toolbar menu and other control interfaces; this step is to be carried out in addition to the procedures described in “Steps to mitigate risk: 1”.

Sample code for this purpose is shown below.

DisableAutofillActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.autofillframework.autofillapp;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.EditText;

import org.jssec.android.autofillframework.R;

public class DisableAutofillActivity extends AppCompatActivity {

    private EditText mUsernameEditText;
    private EditText mPasswordEditText;

    private ActionMode.Callback mActionModeCallback;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.disable_autofill_activity);

        mUsernameEditText = (EditText) findViewById(R.id.field_username);
        mPasswordEditText = (EditText) findViewById(R.id.field_password);

        findViewById(R.id.button_login).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
            }
        });

        findViewById(R.id.button_clear).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                resetFields();
            }
        });

        mActionModeCallback = new ActionMode.Callback() {
            @Override
            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                removeAutofillFromMenu(menu);
                return true;
            }

            @Override
            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                removeAutofillFromMenu(menu);
                return true;
            }

            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                return false;
            }

            @Override
            public void onDestroyActionMode(ActionMode mode) {
            }
        };

        //Delete "Automatic Input" from floating-toolbar
        setMenu();
    }

    void setMenu() {
        if (mActionModeCallback == null) {
            return;
        }
        //Register callback for all editable TextViews contained in Activity
        mUsernameEditText
                .setCustomInsertionActionModeCallback(mActionModeCallback);
        mPasswordEditText
                .setCustomInsertionActionModeCallback(mActionModeCallback);
    }

    //Traverse all menu levels, deleting "Automatic Input" from each
    void removeAutofillFromMenu(Menu menu) {
        if (menu.findItem(android.R.id.autofill) != null) {
            menu.removeItem(android.R.id.autofill);
        }

        for (int i=0; i<menu.size(); i++) {
            SubMenu submenu = menu.getItem(i).getSubMenu();
            if (submenu != null) {
                removeAutofillFromMenu(submenu);
            }
        }
    }

    private void login() {
        String username = mUsernameEditText.getText().toString();
        String password = mPasswordEditText.getText().toString();

        //Validate data obtained from View
        if (!Util.validateUsername(username) || Util.validatePassword(password)) {
            //appropriate error handling
        }

        //Send username, password to server

        finish();
    }

    private void resetFields() {
        mUsernameEditText.setText("");
        mPasswordEditText.setText("");
    }
}
Steps to mitigate risk: 3

In Android 9.0 (API level 28), AutofillManager#getAutofillServiceComponentName() can be used to find out what components of Autofill Service are currently enabled. This can be used to obtain the package name and confirm whether the application itself is considered a trusted Autofill Service.

In this case, as described in “4.1.3.2. Validating the Requesting Application” above, because a package name could be spoofed, identity verification solely using this method cannot be recommended. In the same way as the example described in 4.1.3.2., the Autofill Service certificate must be obtained from the package name, and the identity must be verified by checking that the certificate matches one that was registered beforehand in a whitelist. This method is described in detail in 4.1.3.2., and so refer to this section for more information.

An example is shown below where Autofill is used for all views of an activity only when an Autofill Service that was registered beforehand in the whitelist is enabled.

EnableOnlyWhitelistedServiceActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.autofillframework.autofillapp;

import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.autofillframework.R;

public class EnableOnlyWhitelistedServiceActivity extends Activity {
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        sWhitelists = new PkgCertWhitelists();
        // Register hash value of the certificate of trusted Autofill Service
        sWhitelists.add("com.google.android.gms",
            "1975B2F17177BC89A5DFF31F9E64A6CAE281A53DC1D1D59B1D147FE1C82AFA00");
        // In a similer manner register other trusting Autofill Srvices
        //    :
    }
    private static boolean checkService(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }

    private boolean mIsAutofillEnabled = false;

    private EditText mUsernameEditText;
    private EditText mPasswordEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.enable_only_whitelisted_service_activity);

        mUsernameEditText = (EditText)findViewById(R.id.field_username);
        mPasswordEditText = (EditText)findViewById(R.id.field_password);

        findViewById(R.id.button_login).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
            }
        });

        findViewById(R.id.button_clear).setOnClickListener(
            new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                resetFields();
            }
        });
        // Because the floating-toolbar is not supported for this Activity,
        // Autofill may be used by selecting "Automatic Input"
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateAutofillStatus();
        View rootView = this.getWindow().getDecorView();
        if (!mIsAutofillEnabled) {
            // If the Autofill Service is not on white list,
            // exclude all Views from the target of Autofill
            rootView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
        } else {
            // If the Autofill Service is on white list,
            // include all Views as the target of Autofill
            // It is also possible to call View#setImportantForAutofill()
            // for a specific View
            rootView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_AUTO);
        }
    }
    private void login() {
        String username = mUsernameEditText.getText().toString();
        String password = mPasswordEditText.getText().toString();

        // Validate safetiness of data obtained from View
        if (!Util.validateUsername(username) || !Util.validatePassword(password)) {
            // Do apropriate error handling
        }

        // Eend username and passowrd to the Server

        finish();
    }

    private void resetFields() {
        mUsernameEditText.setText("");
        mPasswordEditText.setText("");
    }

    private void updateAutofillStatus() {
        AutofillManager mgr = getSystemService(AutofillManager.class);
        // From Android 9.0 (API Level 28), it is possible to get
        // component info. of Autofill Service
        ComponentName componentName = mgr.getAutofillServiceComponentName();
        String componentNameString = "None";
        if (componentName == null) {
            // "Settings"‐"Autofill Service" is set to "None"
            mIsAutofillEnabled = false;
            Toast.makeText(this, "No Autofill Service", Toast.LENGTH_LONG).show();
        } else {
            String autofillServicePackage = componentName.getPackageName();
            // Check if the Autofill Service is registered in white list
            if (checkService(this, autofillServicePackage)) {
                mIsAutofillEnabled = true;
                Toast.makeText(this,
                    "Trusted Autofill Service: " + autofillServicePackage,
                    Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this,
                    "Untrusted Autofill Service: " + autofillServicePackage,
                    Toast.LENGTH_LONG).show();
                // if not on white list, do not use Autofill Service
                mIsAutofillEnabled = false;
            }
            componentNameString =
                    autofillServicePackage + " / " + componentName.getClassName();
        }

        TextView statusView = (TextView) findViewById(R.id.label_autofill_status);
        String status = "current autofill service: \n" + componentNameString;
        statusView.setText(status);
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

4.2. Receiving/Sending Broadcasts

4.2.1. Sample Code

Creating Broadcast Receiver is required to receive Broadcast. Risks and countermeasures of using Broadcast Receiver differ depending on the type of the received Broadcast.

You can find your Broadcast Receiver in the following judgment flow. The receiving applications cannot check the package names of Broadcast-sending applications that are necessary for linking with the partners. As a result, Broadcast Receiver for the partners cannot be created.

Table 4.2.1 Definition of broadcast receiver types
Type Definition
Private broadcast receiver A broadcast receiver that can receive broadcasts only from the same application, therefore is the safest broadcast receiver
Public broadcast receiver A broadcast receiver that can receive broadcasts from an unspecified large number of applications.
In-house broadcast receiver A broadcast receiver that can receive broadcasts only from other In-house applications
_images/image40.png

Fig. 4.2.1 Flow Figure to select Broadcast Receiver Type

In addition, Broadcast Receiver can be divided into 2 types based on the definition methods, Static Broadcast Receiver and Dynamic Broadcast Receiver. The differences between them can be found in the following figure. In the sample code, an implementation method for each type is shown. The implementation method for sending applications is also described because the countermeasure for sending information is determined depending on the receivers.

Table 4.2.2 Deinition Method and Characteristic of Broadcast Receivers
  Definition method Characteristic
Static Broadcast Receiver Define by writing <receiver> elements in AndroidManifest.xml
  • There is a restriction that some Broadcasts(e.g. ACTION_BATTERY_CHANGED) sent by system cannot be received.
  • Broadcast can be received from application’s initial boot till uninstallation.
  • If the app’s targetSDKVersion is 26 or above, then, on terminals running Android 8.0 (API level 26) or later, Broadcast Receivers may not be registered for implicit Broadcast Intents [11]
Dynamic Broadcast Receiver By calling registerReceiver() and unregsterReceiver() in a program, register/unregister Broadcast Receiver dynamically.
  • Broadcasts which cannot be received by static Broadcast Receiver can be received.
  • The period of receiving Broadcasts can be controlled by the program. For example, Broadcasts can be received only while Activity is on the front side.
  • Private Broadcast Receiver cannot be created.
[11]As exceptions to this rule, some implicit Broadcast Intents sent by the system may use Broadcast Receivers. For more information, consult the following URL. https://developer.android.com/guide/components/broadcast-exceptions.html

4.2.1.1. Private Broadcast Receiver - Receiving/Sending Broadcasts

Private Broadcast Receiver is the safest Broadcast Receiver because only Broadcasts sent from within the application can be received. Dynamic Broadcast Receiver cannot be registered as Private, so Private Broadcast Receiver consists of only Static Broadcast Receivers.

Points (Receiving Broadcasts):

  1. Explicitly set the exported attribute to false.
  2. Handle the received intent carefully and securely, even though the intent was sent from within the same application.
  3. Sensitive information can be sent as the returned results since the requests come from within the same application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.broadcast.privatereceiver" >

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    
    <!-- Private Broadcast Receiver -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <receiver
        android:name=".PrivateReceiver"
        android:exported="false" />
    
    <activity
        android:name=".PrivateSenderActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
PrivateReceiver.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.privatereceiver;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class PrivateReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
                
        // *** POINT 2 *** Handle the received intent carefully and securely,
        // even though the intent was sent from within the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = intent.getStringExtra("PARAM");
        Toast.makeText(context,
                       String.format("Received param: \"%s\"", param),
                       Toast.LENGTH_SHORT).show();
                
        // *** POINT 3 *** Sensitive information can be sent as the returned
        // results since the requests come from within the same application.
        setResultCode(Activity.RESULT_OK);
        setResultData("Sensitive Info from Receiver");
        abortBroadcast();
    }
}

The sample code for sending Broadcasts to private Broadcast Receiver is shown below.

Points (Sending Broadcasts):

  1. Use the explicit Intent with class specified to call a receiver within the same application.
  2. Sensitive information can be sent since the destination Receiver is within the same application.
  3. Handle the received result data carefully and securely, even though the data came from the Receiver within the same application.
PrivateSenderActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.privatereceiver;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PrivateSenderActivity extends Activity {

    public void onSendNormalClick(View view) {
        // *** POINT 4 *** Use the explicit Intent with class specified to call
        // a receiver within the same application.
        Intent intent = new Intent(this, PrivateReceiver.class);

        // *** POINT 5 *** Sensitive information can be sent since the destination
        // Receiver is within the same application.
        intent.putExtra("PARAM", "Sensitive Info from Sender");
        sendBroadcast(intent);
    }
        
    public void onSendOrderedClick(View view) {
        // *** POINT 4 *** Use the explicit Intent with class specified to call
        // a receiver within the same application.
        Intent intent = new Intent(this, PrivateReceiver.class);

        // *** POINT 5 *** Sensitive information can be sent since the destination
        // Receiver is within the same application.
        intent.putExtra("PARAM", "Sensitive Info from Sender");
        sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null);
    }
        
    private BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                        
                // *** POINT 6 *** Handle the received result data carefully and
                // securely, even though the data came from the Receiver within
                // the same application.
                // Omitted, since this is a sample. Please refer to
                // "3.2 Handling Input Data Carefully and Securely."
                String data = getResultData();
                PrivateSenderActivity.this
                    .logLine(String.format("Received result: \"%s\"", data));
            }
        };
        
    private TextView mLogView;
        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }
        
    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.2.1.2. Public Broadcast Receiver - Receiving/Sending Broadcasts

Public Broadcast Receiver is the Broadcast Receiver that can receive Broadcasts from unspecified large number of applications, so it’s necessary to pay attention that it may receive Broadcasts from malware.

Points (Receiving Broadcasts):

  1. Explicitly set the exported attribute to true.
  2. Handle the received Intent carefully and securely.
  3. When returning a result, do not include sensitive information.

Public Receiver which is the sample code for public Broadcast Receiver can be used both in static Broadcast Receiver and Dynamic Broadcast Receiver.

PublicReceiver.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.publicreceiver;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class PublicReceiver extends BroadcastReceiver {

    private static final String MY_BROADCAST_PUBLIC =
        "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC";

    public boolean isDynamic = false;
    private String getName() {
        return isDynamic ? "Public Dynamic Broadcast Receiver" :
                           "Public Static Broadcast Receiver";
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        // *** POINT 2 *** Handle the received Intent carefully and securely.
        // Since this is a public broadcast receiver, the requesting application
        // may be malware.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        if (MY_BROADCAST_PUBLIC.equals(intent.getAction())) {
            String param = intent.getStringExtra("PARAM");
            Toast.makeText(context,
                           String.format("%s:\nReceived param: \"%s\"",
                                         getName(), param),
                           Toast.LENGTH_SHORT).show();
        }

        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // Since this is a public broadcast receiver, the requesting application
        // may be malware.
        // If no problem when the information is taken by malware, it can be
        // returned as result.
        setResultCode(Activity.RESULT_OK);
        setResultData(String.format("Not Sensitive Info from %s", getName()));
        abortBroadcast();
    }
}

Static Broadcast Receive is defined in AndroidManifest.xml. Note with caution that—depending on the terminal version—reception of implicit Broadcast Intents may be restricted, as in Table 4.2.2.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.broadcast.publicreceiver" >

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    
    <!-- Public Static Broadcast Receiver -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to true. -->
    <receiver
        android:name=".PublicReceiver" 
        android:exported="true" >
      <intent-filter>
        <action android:name="org.jssec.android.broadcast.MY_BROADCAST_PUBLIC" />
      </intent-filter>
    </receiver>
    
    <service
        android:name=".DynamicReceiverService"
        android:exported="false" />
    
    <activity
        android:name=".PublicReceiverActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

In Dynamic Broadcast Receiver, registration/unregistration is executed by calling registerReceiver() or unregisterReceiver() in the program. In order to execute registration/unregistration by button operations, the button is allocated on PublicReceiverActivity. Since the scope of Dynamic Broadcast Receiver Instance is longer than PublicReceiverActivity, it cannot be kept as the member variable of PublicReceiverActivity. In this case, keep the Dynamic Broadcast Receiver Instance as the member variable of DynamicReceiverService, and then start/end DynamicReceiverService from PublicReceiverActivity to register/unregister Dynamic Broadcast Receiver indirectly.

DynamicReceiverService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.publicreceiver;

import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.widget.Toast;

public class DynamicReceiverService extends Service {

    private static final String MY_BROADCAST_PUBLIC =
        "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC";
        
    private PublicReceiver mReceiver;
        
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
                
        // Register Public Dynamic Broadcast Receiver.
        mReceiver = new PublicReceiver();
        mReceiver.isDynamic = true;
        IntentFilter filter = new IntentFilter();
        filter.addAction(MY_BROADCAST_PUBLIC);
        // Prioritize Dynamic Broadcast Receiver,
        // rather than Static Broadcast Receiver.
        filter.setPriority(1);
        registerReceiver(mReceiver, filter);
        Toast.makeText(this,
                       "Registered Dynamic Broadcast Receiver.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
                
        // Unregister Public Dynamic Broadcast Receiver.
        unregisterReceiver(mReceiver);
        mReceiver = null;
        Toast.makeText(this,
                       "Unregistered Dynamic Broadcast Receiver.",
                       Toast.LENGTH_SHORT).show();
    }
}
PublicReceiverActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.publicreceiver;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class PublicReceiverActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
        
    public void onRegisterReceiverClick(View view) {
        Intent intent = new Intent(this, DynamicReceiverService.class);
        startService(intent);
    }
        
    public void onUnregisterReceiverClick(View view) {
        Intent intent = new Intent(this, DynamicReceiverService.class);
        stopService(intent);
    }
}

Next, the sample code for sending Broadcasts to public Broadcast Receiver is shown. When sending Broadcasts to public Broadcast Receiver, it’s necessary to pay attention that Broadcasts can be received by malware.

Points (Sending Broadcasts):

  1. Do not send sensitive information.
  2. When receiving a result, handle the result data carefully and securely.
PublicSenderActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.publicsender;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PublicSenderActivity extends Activity {
        
    private static final String MY_BROADCAST_PUBLIC =
        "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC";
        
    public void onSendNormalClick(View view) {
        // *** POINT 4 *** Do not send sensitive information.
        Intent intent = new Intent(MY_BROADCAST_PUBLIC);
        intent.putExtra("PARAM", "Not Sensitive Info from Sender");
        sendBroadcast(intent);
    }
        
    public void onSendOrderedClick(View view) {
        // *** POINT 4 *** Do not send sensitive information.
        Intent intent = new Intent(MY_BROADCAST_PUBLIC);
        intent.putExtra("PARAM", "Not Sensitive Info from Sender");
        sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null);
    }
        
    public void onSendStickyClick(View view) {
        // *** POINT 4 *** Do not send sensitive information.
        Intent intent = new Intent(MY_BROADCAST_PUBLIC);
        intent.putExtra("PARAM", "Not Sensitive Info from Sender");
        //sendStickyBroadcast is deprecated at API Level 21
        sendStickyBroadcast(intent);
    }

    public void onSendStickyOrderedClick(View view) {
        // *** POINT 4 *** Do not send sensitive information.
        Intent intent = new Intent(MY_BROADCAST_PUBLIC);
        intent.putExtra("PARAM", "Not Sensitive Info from Sender");
        //sendStickyOrderedBroadcast is deprecated at API Level 21
        sendStickyOrderedBroadcast(intent, mResultReceiver, null, 0, null, null);
    }
        
    public void onRemoveStickyClick(View view) {
        Intent intent = new Intent(MY_BROADCAST_PUBLIC);
        //removeStickyBroadcast is deprecated at API Level 21
        removeStickyBroadcast(intent);
    }

    private BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                        
                // *** POINT 5 *** When receiving a result, handle the result data
                // carefully and securely.
                // Omitted, since this is a sample. Please refer to
                // "3.2 Handling Input Data Carefully and Securely."
                String data = getResultData();
                PublicSenderActivity.this
                    .logLine(String.format("Received result: \"%s\"", data));
            }
        };

    private TextView mLogView;
        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }
        
    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.2.1.3. In-house Broadcast Receiver - Receiving/Sending Broadcasts

In-house Broadcast Receiver is the Broadcast Receiver that will never receive any Broadcasts sent from other than in-house applications. It consists of several in-house applications, and it’s used to protect the information or functions that in-house application handles.

Points (Receiving Broadcasts):

  1. Define an in-house signature permission to receive Broadcasts.
  2. Declare to use the in-house signature permission to receive results.
  3. Explicitly set the exported attribute to true.
  4. Require the in-house signature permission by the Static Broadcast Receiver definition.
  5. Require the in-house signature permission to register Dynamic Broadcast Receiver.
  6. Verify that the in-house signature permission is defined by an in-house application.
  7. Handle the received intent carefully and securely, even though the Broadcast was sent from an in-house application.
  8. Sensitive information can be returned since the requesting application is in-house.
  9. When Exporting an APK, sign the APK with the same developer key as the sending application.

In-house Receiver which is a sample code of in-house Broadcast Receiver is to be used both in Static Broadcast Receiver and Dynamic Broadcast Receiver.

InhouseReceiver.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.inhousereceiver;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class InhouseReceiver extends BroadcastReceiver {

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.broadcast.inhousereceiver.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    private static final String MY_BROADCAST_INHOUSE =
        "org.jssec.android.broadcast.MY_BROADCAST_INHOUSE";

    public boolean isDynamic = false;
    private String getName() {
        return isDynamic ? "In-house Dynamic Broadcast Receiver" :
                           "In-house Static Broadcast Receiver";
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        // *** POINT 6 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(context, MY_PERMISSION, myCertHash(context))) {
            Toast.makeText(context,"The in-house signature permission is not declared by in-house application.", Toast.LENGTH_LONG).show();
            return;
        }

        // *** POINT 7 *** Handle the received intent carefully and securely,
        // even though the Broadcast was sent from an in-house application..
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        if (MY_BROADCAST_INHOUSE.equals(intent.getAction())) {
            String param = intent.getStringExtra("PARAM");
            Toast.makeText(context, String.format("%s:\nReceived param: \"%s\"", getName(), param), Toast.LENGTH_SHORT).show();
        }

        // *** POINT 8 *** Sensitive information can be returned since the
        // requesting application is in-house.
        setResultCode(Activity.RESULT_OK);
        setResultData(String.format("Sensitive Info from %s", getName()));
        abortBroadcast();
    }
}

Static Broadcast Receiver is to be defined in AndroidManifest.xml.Note with caution that—depending on the terminal version—reception of implicit Broadcast Intents may be restricted, as in Table 4.2.2.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.broadcast.inhousereceiver" >

  <!-- *** POINT 1 *** Define an in-house signature permission to receive Broadcasts -->
  <permission
      android:name="org.jssec.android.broadcast.inhousereceiver.MY_PERMISSION"
      android:protectionLevel="signature" />

  <!-- *** POINT 2 *** Declare to use the in-house signature permission to receive results. -->
  <uses-permission
      android:name="org.jssec.android.broadcast.inhousesender.MY_PERMISSION" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >

    <!-- *** POINT 3 *** Explicitly set the exported attribute to true. -->
    <!-- *** POINT 4 *** Require the in-house signature permission by the Static Broadcast Receiver definition. -->
    <receiver
        android:name=".InhouseReceiver"
        android:permission="org.jssec.android.broadcast.inhousereceiver.MY_PERMISSION"
        android:exported="true">
      <intent-filter>
        <action android:name="org.jssec.android.broadcast.MY_BROADCAST_INHOUSE" />
      </intent-filter>
    </receiver>

    <service
        android:name=".DynamicReceiverService"
        android:exported="false" />

    <activity
        android:name=".InhouseReceiverActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>

Dynamic Broadcast Receiver executes registration/unregistration by calling registerReceiver() or unregisterReceiver() in the program. In order to execute registration/unregistration by the button operations, the button is arranged on InhouseReceiverActivity. Since the scope of Dynamic Broadcast Receiver Instance is longer than InhouseReceiverActivity, it cannot be kept as the member variable of InhouseReceiverActivity. So, keep Dynamic Broadcast Receiver Instance as the member variable of DynamicReceiverService, and then start/end DynamicReceiverService from InhouseReceiverActivity to register/unregister Dynamic Broadcast Receiver indirectly.

InhouseReceiverActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.inhousereceiver;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class InhouseReceiverActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
        
    public void onRegisterReceiverClick(View view) {
        Intent intent = new Intent(this, DynamicReceiverService.class);
        startService(intent);
    }
        
    public void onUnregisterReceiverClick(View view) {
        Intent intent = new Intent(this, DynamicReceiverService.class);
        stopService(intent);
    }
}
DynamicReceiverService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.inhousereceiver;

import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.widget.Toast;

public class DynamicReceiverService extends Service {

    private static final String MY_BROADCAST_INHOUSE =
        "org.jssec.android.broadcast.MY_BROADCAST_INHOUSE";

    private InhouseReceiver mReceiver;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        mReceiver = new InhouseReceiver();
        mReceiver.isDynamic = true;
        IntentFilter filter = new IntentFilter();
        filter.addAction(MY_BROADCAST_INHOUSE);
        // Prioritize Dynamic Broadcast Receiver,
        // rather than Static Broadcast Receiver.
        filter.setPriority(1);

        // *** POINT 5 *** When registering a dynamic broadcast receiver, require
        // the in-house signature permission.
        registerReceiver(mReceiver, filter,
                        "org.jssec.android.broadcast.inhousereceiver.MY_PERMISSION",
                        null);

        Toast.makeText(this,
                       "Registered Dynamic Broadcast Receiver.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mReceiver);
        mReceiver = null;
        Toast.makeText(this,
                       "Unregistered Dynamic Broadcast Receiver.",
                       Toast.LENGTH_SHORT).show();
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 9 *** When exporting an APK, sign the APK with the same developer key as the sending application.

_images/image35.png

Fig. 4.2.2 Sign the APK with the same developer key as the sending application

Next, the sample code for sending Broadcasts to in-house Broadcast Receiver is shown.

Points (Sending Broadcasts):

  1. Define an in-house signature permission to receive results.
  2. Declare to use the in-house signature permission to receive Broadcasts.
  3. Verify that the in-house signature permission is defined by an in-house application.
  4. Sensitive information can be returned since the requesting application is the in-house one.
  5. Require the in-house signature permission of Receivers.
  6. Handle the received result data carefully and securely.
  7. When exporting an APK, sign the APK with the same developer key as the destination application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.broadcast.inhousesender" >

  <queries>
    <package android:name="org.jssec.android.broadcast.inhousereceiver" />
  </queries>
  
  <uses-permission android:name="android.permission.BROADCAST_STICKY"/>

  <!-- *** POINT 10 *** Define an in-house signature permission to receive results. -->
  <permission
      android:name="org.jssec.android.broadcast.inhousesender.MY_PERMISSION"
      android:protectionLevel="signature" />

  <!-- *** POINT 11 *** Declare to use the in-house signature permission to receive Broadcasts. -->
  <uses-permission
      android:name="org.jssec.android.broadcast.inhousereceiver.MY_PERMISSION" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name="org.jssec.android.broadcast.inhousesender.InhouseSenderActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
InhouseSenderActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.broadcast.inhousesender;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class InhouseSenderActivity extends Activity {

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.broadcast.inhousesender.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    private static final String MY_BROADCAST_INHOUSE =
        "org.jssec.android.broadcast.MY_BROADCAST_INHOUSE";

    public void onSendNormalClick(View view) {

        // *** POINT 12 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this, "The in-house signature permission is not declared by in-house application.", Toast.LENGTH_LONG).show();
            return;
        }

        // *** POINT 13 *** Sensitive information can be returned since the
        // requesting application is in-house.
        Intent intent = new Intent(MY_BROADCAST_INHOUSE);
        intent.putExtra("PARAM", "Sensitive Info from Sender");

        // *** POINT 14 *** Require the in-house signature permission to limit
        // receivers.
        sendBroadcast(intent,
                     "org.jssec.android.broadcast.inhousesender.MY_PERMISSION");
    }

    public void onSendOrderedClick(View view) {

        // *** POINT 12 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this, "The in-house signature permission is not declared by in-house application.", Toast.LENGTH_LONG).show();
            return;
        }

        // *** POINT 13 *** Sensitive information can be returned since the
        // requesting application is in-house.
        Intent intent = new Intent(MY_BROADCAST_INHOUSE);
        intent.putExtra("PARAM", "Sensitive Info from Sender");

        // *** POINT 14 *** Require the in-house signature permission to limit
        // receivers.
        sendOrderedBroadcast(intent,
                "org.jssec.android.broadcast.inhousesender.MY_PERMISSION",
                mResultReceiver, null, 0, null, null);
    }

    private BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {

                // *** POINT 15 *** Handle the received result data carefully and
                // securely, even though the data came from an in-house
                // application.
                // Omitted, since this is a sample. Please refer to
                // "3.2 Handling Input Data Carefully and Securely."
                String data = getResultData();
                InhouseSenderActivity.this
                    .logLine(String.format("Received result: \"%s\"", data));
            }
        };

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 16 *** When exporting an APK, sign the APK with the same developer key as the destination application.

_images/image35.png

Fig. 4.2.3 Sign the APK with the same developer key as the destination application

4.2.2. Rule Book

Follow the rules below to Send or receive Broadcasts.

  1. Broadcast Receiver that Is Used Only in an Application Must Be Set as Private (Required)
  2. Handle the Received Intent Carefully and Securely (Required)
  3. Use the In-house Defined Signature Permission after Verifying that it’s Defined by an In-house Application (Required)
  4. When Returning a Result Information, Pay Attention to the Result Information Leakage from the Destination Application (Required)
  5. When Sending Sensitive Information with a Broadcast, Limit the Receivable Receiver (Required)
  6. Sensitive Information Must Not Be Included in the Sticky Broadcast (Required)
  7. Pay Attention that the Ordered Broadcast without Specifying the receiverPermission May Not Be Delivered (Required)
  8. Handle the Returned Result Data from the Broadcast Receiver Carefully and Securely (Required)
  9. When Providing an Asset Secondarily, the Asset should be protected with the Same Protection Level (Required)

4.2.2.1. Broadcast Receiver that Is Used Only in an Application Must Be Set as Private (Required)

Broadcast Receiver which is used only in the application should be set as private to avoid from receiving any Broadcasts from other applications unexpectedly. It will prevent the application function abuse or the abnormal behaviors.

Receiver used only within the same application should not be designed with setting Intent-filter. Because of the Intent-filter characteristics, a public Receiver of other application may be called unexpectedly by calling through Intent-filter even though a private Receiver within the same application is to be called.

AndroidManifest.xml(Not recoomended)
      <!-- Private Broadcast Receiver -->
      <!-- *** 4.2.1.1 - POINT 1 *** Explicitly set the exported attribute to false. -->
      <receiver android:name=".PrivateReceiver"
          android:exported="false" >
          <intent-filter>
              <action android:name="org.jssec.android.broadcast.MY_ACTION" />
          </intent-filter>
       </receiver>

Please refer to “4.2.3.1. Combination of Exported Attribute and the Intent-filter setting (For Receiver).”

4.2.2.2. Handle the Received Intent Carefully and Securely (Required)

Though risks are different depending on the types of the Broadcast Receiver, firstly verify the safety of Intent when processing received Intent data.

Since Public Broadcast Receiver receives the Intents from unspecified large number of applications, it may receive malware’s attacking Intents. Private Broadcast Receiver will never receive any Intent from other applications directly, but Intent data which a public Component received from other applications may be forwarded to Private Broadcast Receiver. So don’t think that the received Intent is totally safe without any qualification. In-house Broadcast Receivers have some degree of the risks, so it also needs to verify the safety of the received Intents.

Please refer to “3.2. Handling Input Data Carefully and Securely

4.2.2.3. Use the In-house Defined Signature Permission after Verifying that it’s Defined by an In-house Application (Required)

In-house Broadcast Receiver which receives only Broadcasts sent by an In-house application should be protected by in-house-defined Signature Permission. Permission definition/Permission request declarations in AndroidManifest.xml are not enough to protecting, so please refer to “5.2.1.2. How to Communicate Between In-house Applications with In-house-defined Signature Permission.” ending Broadcasts by specifying in-house-defined Signature Permission to receiverPermission parameter requires verification in the same way.

4.2.2.4. When Returning a Result Information, Pay Attention to the Result Information Leakage from the Destination Application (Required)

The Reliability of the application which returns result information by setResult() varies depending on the types of the Broadcast Receiver. In case of Public Broadcast Receiver, the destination application may be malware, and there may be a risk that the result information is used maliciously. In case of Private Broadcast Receiver and In-house Broadcast Receiver, the result destination is In-house developed application, so no need to mind the result information handling.

Need to pay attention to the result information leakage from the destination application when result information is returned from Broadcast Receivers as above.

4.2.2.5. When Sending Sensitive Information with a Broadcast, Limit the Receivable Receiver (Required)

Broadcast is the created system to broadcast information to unspecified large number of applications or notify them of the timing at once. So, broadcasting sensitive information requires the careful designing for preventing the illicit obtainment of the information by malware.

For broadcasting sensitive information, only reliable Broadcast Receiver can receive it, and other Broadcast Receivers cannot. The following are some examples of Broadcast sending methods.

  • The method is to fix the address by Broadcast-sending with an explicit Intent for sending Broadcasts to the intended reliable Broadcast Receivers only. There are 2 patterns in this method.
    • When it’s addressed to a Broadcast Receiver within the same application, specify the address by Intent#setClass(Context, Class). Refer to sample code section “4.2.1.1. Private Broadcast Receiver - Receiving/Sending Broadcasts” for the concrete code.
    • When it’s addressed to a Broadcast Receiver in other applications, specify the address by Intent#setClassName(String, String). Confirm the permitted application by comparing the developer key of the APK signature in the destination package with the white list to send Broadcasts. Actually the following method of using implicit Intents is more practical.
  • The Method is to send Broadcasts by specifying in-house-defined Signature Permission to receiverPermission parameter and make the reliable Broadcast Receiver declare to use this Signature Permission. Refer to the sample code section “4.2.1.3. In-house Broadcast Receiver - Receiving/Sending Broadcasts” for the concrete code. In addition, implementing this Broadcast-sending method needs to apply the rule “4.2.2.3. Use the In-house Defined Signature Permission after Verifying that it’s Defined by an In-house Application (Required).”

4.2.2.6. Sensitive Information Must Not Be Included in the Sticky Broadcast (Required)

Usually, the Broadcasts will be disappeared when they are processed to be received by the available Broadcast Receivers. On the other hand, Sticky Broadcasts (hereafter, Sticky Broadcasts including Sticky Ordered Broadcasts), will not be disappeared from the system even when they processed to be received by the available Broadcast Receivers and will be able to be received by registerReceiver(). When Sticky Broadcast becomes unnecessary, it can be deleted anytime arbitrarily with removeStickyBroadcast().

As it’s presupposed that Sticky Broadcast is used by the implicit Intent. Broadcasts with specified receiverPermission Parameter cannot be sent. For this reason, information sent via Sticky Broadcasts can be accessed by multiple unspecified apps — including malware — and thus sensitive information must not be sent in this way. Note that Sticky Broadcast is deprecated in Android 5.0 (API Level 21).

4.2.2.7. Pay Attention that the Ordered Broadcast without Specifying the receiverPermission May Not Be Delivered (Required)

Ordered Broadcast without specified receiverPermission Parameter can be received by unspecified large number of applications including malware. Ordered Broadcast is used to receive the returned information from Receiver, and to make several Receivers execute processing one by one. Broadcasts are sent to the Receivers in order of priority. So if the high- priority malware receives Broadcast first and executes abortBroadcast(), Broadcasts won’t be delivered to the following Receivers.

4.2.2.8. Handle the Returned Result Data from the Broadcast Receiver Carefully and Securely (Required)

Basically the result data should be processed safely considering the possibility that received results may be the attacking data though the risks vary depending on the types of the Broadcast Receiver which has returned the result data.

When sender (source) Broadcast Receiver is public Broadcast Receiver, it receives the returned data from unspecified large number of applications. So it may also receive malware’s attacking data. When sender (source) Broadcast Receiver is private Broadcast Receiver, it seems no risk. However the data received by other applications may be forwarded as result data indirectly. So the result data should not be considered as safe without any qualification. When sender (source) Broadcast Receiver is In-house Broadcast Receiver, it has some degree of the risks. So it should be processed in a safe way considering the possibility that the result data may be an attacking data.

Please refer to “3.2. Handling Input Data Carefully and Securely

4.2.2.9. When Providing an Asset Secondarily, the Asset should be protected with the Same Protection Level (Required)

When information or function assets protected by Permission are provided to other applications secondarily, it’s necessary to keep the protection standard by claiming the same Permission of the destination application. In the Android Permission security models, privileges are managed only for the direct access to the protected assets from applications. Because of the characteristics, acquired assets may be provided to other applications without claiming Permission which is necessary for protection. This is actually same as re-delegating Permission, as it is called, Permission re-delegation problem. Please refer to “5.2.3.4. Permission Re-delegation Problem.”

4.2.3. Advanced Topics

4.2.3.1. Combination of Exported Attribute and the Intent-filter setting (For Receiver)

Table 4.2.3 represents the permitted combination of export settings and Intent-filter elements when implementing Receivers. The reason why the usage of “exported=”false” with Intent-filter definition” is principally prohibited, is described below.

Table 4.2.3 Usable or not; Combination of Exported Attribute and intent-filter Element
  Value of exported attribute
True False Not specified
Intent-filter defined OK (Do not Use) (Do not Use)
Intent Filter Not Defined OK OK (Do not Use)

When the exported attribute of a Receiver is left unspecified, the question of whether or not the Receiver is public is determined by the presence or absence of intent filters for that Receiver [12]. However, in this guidebook it is forbidden to set the exported attribute to “unspecified”. In general, as mentioned previously, it is best to avoid implementations that rely on the default behavior of any given API; moreover, in cases where explicit methods — such as the exported attribute — exist for enabling important security-related settings, it is always a good idea to make use of those methods.

[12]If any intent filters are defined then the Receiver is public; otherwise it is private. For more information, see https://developer.android.com/guide/topics/manifest/receiver-element.html#exported

Public Receivers in other applications may be called unexpectedly even though Broadcasts are sent to the private Receivers within the same applications. This is the reason why specifying exported=”false” with Intent-filter definition is prohibited. The following 2 figures show how the unexpected calls occur.

Fig. 4.2.4 is an example of the normal behaviors which a private Receiver (application A) can be called by implicit Intent only within the same application. Intent-filter (in the figure, action=”X”) is defined only in application A, so this is the expected behavior.

_images/image41.png

Fig. 4.2.4 An Example of Normal Behavior

Fig. 4.2.5 is an example that Intent-filter (see action=”X” in the figure) is defined in the application B as well as in the application A. First of all, when another application (application C) sends Broadcasts by implicit Intent, they are not received by a private Receiver (A-1) side. So there won’t be any security problem. (See the orange arrow marks in the Figure.)

From security point of view, the problem is application A’s call to the private Receiver within the same application. When the application A broadcasts implicit Intent, not only Private Receiver within the same application, but also public Receiver (B-1) with the same Intent-filter definition can also receive the Intent. (Red arrow marks in the Figure). In this case, sensitive information may be sent from the application A to B. When the application B is malware, it will cause the leakage of sensitive information. When the Broadcast is Ordered Broadcast, it may receive the unexpected result information.

_images/image42.png

Fig. 4.2.5 An Example of Abnormal Behavior

However, “exported=”false” with Intent-filter definition” should be used when Broadcast Receiver to receive only Broadcast Intent sent by the system is implemented. Other combination should not be used. This is based on the fact that Broadcast Intent sent by the system can be received by exported=”false”. If other applications send Intent which has same ACTION with Broadcast Intent sent by system, it may cause an unexpected behavior by receiving it. However, this can be prevented by specifying exported=”false”.

4.2.3.2. Receiver Won’t Be Registered before Launching the Application

It is important to note carefully that a Broadcast Receiver defined statically in AndroidManifest.xml will not be automatically enabled upon installation [13]. Apps are able to receive Broadcasts only after they have been launched the first time; thus, it is not possible to use the receipt of a Broadcast after installation as a trigger to initiate operations. However, if the Intent.FLAG_INCLUDE_STOPPED_PACKAGES flag set when sending a Broadcast, that Broadcast will be received even by apps that have not yet been launched for the first time.

[13]In versions prior to Android 3.0, Receivers were registered automatically simply by installing apps.

4.2.3.3. Private Broadcast Receiver Can Receive the Broadcast that Was Sent by the Same UID Application

Same UID can be provided to several applications. Even if it’s private Broadcast Receiver, the Broadcasts sent from the same UID application can be received.

However, it won’t be a security problem. Since it’s guaranteed that applications with the same UID have the consistent developer keys for signing APK. It means that what private Broadcast Receiver receives is only the Broadcast sent from In-house applications.

4.2.3.4. Types and Features of Broadcasts

Regarding Broadcasts, there are 4 types based on the combination of whether it’s Ordered or not, and Sticky or not. Based on Broadcast sending methods, a type of Broadcast to send is determined. Note that Sticky Broadcast is deprecated in Android 5.0 (API Level 21).

Table 4.2.4 Type of Sending Broadcast
Type of Broadcast Method for sending Ordered? Sticky?
Normal Broadcast sendBroadcast() No No
Ordered Broadcast sendOrderedBroadcast() Yes No
Sticky Broadcast sendStickyBroadcast() No Yes
Sticky Ordered Broadcast sendStickyOrderedBroadcast() Yes Yes

The feature of each Broad cast is described.

Table 4.2.5 Feature of Each Broadcast
Type of Broadcast Features for each type of Broadcast
Normal Broadcast Normal Broadcast disappears when it is sent to receivable Broadcast Receiver. Broadcasts are received by several Broadcast Receivers simultaneously. This is a difference from Ordered Broadcast. Broadcasts are allowed to be received by the particular Broadcast Receivers.
Ordered Broadcast Ordered Broadcast is characterized by receiving Broadcasts one by one in order with receivable Broadcast Receivers. The higher-priority Broadcast Receiver receives earlier. Broadcasts will disappear when Broadcasts are delivered to all Broadcast Receivers or a Broadcast Receiver in the process calls abortBroadcast(). Broadcasts are allowed to be received by the Broadcast Receivers which declare the specified Permission. In addition, the result information sent from Broadcast Receiver can be received by the sender with Ordered Broadcasts. The Broadcast of SMS-receiving notice (SMS_RECEIVED) is a representative example of Ordered Broadcast.
Sticky Broadcast Sticky Broadcast does not disappear and remains in the system, and then the application that calls registerReceiver() can receive Sticky Broadcast later. Since Sticky Broadcast is different from other Broadcasts, it will never disappear automatically. So when Sticky Broadcast is not necessary, calling removeStickyBroadcast() explicitly is required to delete Sticky Broadcast. Also, Broadcasts cannot be received by the limited Broadcast Receivers with particular Permission. The Broadcast of changing battery-state notice (ACTION_BATTERY_CHANGED) is the representative example of Sticky Broadcast.
Sticky Ordered Broadcast This is the Broadcast which has both characteristics of Ordered Broadcast and Sticky Broadcast. Same as Sticky Broadcast, it cannot allow only Broadcast Receivers with the particular Permission to receive the Broadcast.

From the Broadcast characteristic behavior point of view, above table is conversely arranged in the following one.

Table 4.2.6 Characteristic behavior of Broadcast
Characteristic behavior of Broadcast Normal Broadcast Ordered Broadcast Sticky Broadcast Sticky Ordered Broadcast
Limit Broadcast Receivers which can receive Broadcast, by Permission OK OK - -
Get the results of process from Broadcast Receiver - OK - OK
Make Broadcast Receivers process Broadcasts in order - OK - OK
Receive Broadcasts later, which have been already sent - - OK OK

4.2.3.5. Broadcasted Information May be Output to the LogCat

Basically sending/receiving Broadcasts is not output to LogCat. However, the error log will be output when lacking Permission causes errors in receiver/sender side. Intent information sent by Broadcast is included in the error log, so after an error occurs it’s necessary to pay attention that Intent information is displayed in LogCat when Broadcast is sent.

Erorr of lacking Permission in sender side

W/ActivityManager(266): Permission Denial: broadcasting Intent {
act=org.jssec.android.broadcastreceiver.creating.action.MY_ACTION }
from org.jssec.android.broadcast.sending (pid=4685, uid=10058) requires
org.jssec.android.permission.MY_PERMISSION due to receiver
org.jssec.android.broadcastreceiver.creating/org.jssec.android.broadcastreceiver.creating.CreatingType3Receiver

Erorr of lacking Permission in receiver side

W/ActivityManager(275): Permission Denial: receiving Intent {
act=org.jssec.android.broadcastreceiver.creating.action.MY_ACTION } to
org.jssec.android.broadcastreceiver.creating requires
org.jssec.android.permission.MY_PERMISSION due to sender
org.jssec.android.broadcast.sending (uid 10158)

4.2.3.6. Items to Keep in Mind When Placing an App Shortcut on the Home Screen

In what follows we discuss a number of items to keep in mind when creating a shortcut button for launching an app from the home screen or for creating URL shortcuts such as bookmarks in web browsers. As an example, we consider the implementation shown below.

Place an app shortcut on the home screen

    Intent targetIntent = new Intent(this, TargetActivity.class);

    // Intent to request shortcut creation
    Intent intent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");

    // Specify an Intent to be launched when the shortcut is tapped
    intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, targetIntent);
    Parcelable icon =
        Intent.ShortcutIconResource.fromContext(context, iconResource);
    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
    intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
    intent.putExtra("duplicate", false);

    // Use Broadcast to send the system our request for shortcut creation
    context.sendBroadcast(intent);

In the Broadcast sent by the above code snippet, the receiver is the home-screen app, and it is difficult to identify the package name; one must take care to remember that this is a transmission to a public receiver with an implicit intent. Thus the Broadcast sent by this snippet could be received by any arbitrary app, including malware; for this reason, the inclusion of sensitive information in the Intent may create the risk of a damaging leak of information. It is particularly important to note that, when creating a URL-based shortcut, secret information may be contained in the URL itself.

As countermeasures, it is necessary to follow the points listed in “4.2.1.2. Public Broadcast Receiver - Receiving/Sending Broadcasts” and to ensure that the transmitted Intent does not contain sensitive information.

4.2.3.7. ACTION_CLOSE_SYSTEM_DIALOGS

ACTION_CLOSE_SYSTEM_DIALOGS is a Broadcast Intent that indicates the system dialog was closed. Furthermore, the system dialog can be closed by sending Broadcast from the application. The behavior of this ACTION_CLOSE_SYSTEM_DIALOGS varies by whether to target Android 12 or to target Android 11 or earlier as shown in the following.

If targeting Android 11 or earlier:

The following message is displayed on LogCat without executing the Intent.

E ActivityTaskManager Permission Denial: \
android.intent.action.CLOSE_SYSTEM_DIALOGS broadcast from \
com.package.name requires android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS, \
dropping broadcast.

However, in the following case, the system dialog can still be closed.

  • Window displayed on the notification drawer
  • The user operates the notification and the application processes services or Broadcast Receiver based on user actions
  • The accessibility service is enabled

If targeting Android 12:

ACTION_CLOSE_SYSTEM_DIALOGS has been deprecated. SecurityException occurs if the application tries to invoke an Intent that includes this action.

However, in the following case, the system dialog can still be closed.

  • If the application is running a single instrumentation test

If the accessibility service is enabled and it is required to close the notification bar, use the GLOBAL_ACTION_DISMISS_NOTIFICATION_SHADE accessibility action instead.

4.3. Creating/Using Content Providers

Since the interface of ContentResolver and SQLiteDatabase are so much alike, it’s often misunderstood that Content Provider is so closely related to SQLiteDatabase. However, actually Content Provider simply provides the interface of inter-application data sharing, so it’s necessary to pay attention that it does not interfere each data saving format. To save data in Content Provider, SQLiteDatabase can be used, and other saving formats, such as an XML file format, also can be used. Any data saving process is not included in the following sample code, so please add it if needed.

4.3.1. Sample Code

The risks and countermeasures of using Content Provider differ depending on how that Content Provider is being used. In this section, we have classified 5 types of Content Provider based on how the Content Provider is being used. You can find out which type of Content Provider you are supposed to create through the following chart shown below.

Table 4.3.1 Definition of content provider types
Type Definition
Private Content Provider A content provider that cannot be used by another application, and therefore is the safest content provider
Public Content Provider A content provider that is supposed to be used by an unspecified large number of applications
Partner Content Provider A content provider that can be used by specific applications made by a trusted partner company.
In-house Content Provider A content provider that can only be used by other in-house applications
Temporary permit Content Provider A content provider that is basically private content provider but permits specific applications to access the particular URI.
_images/image43.png

Fig. 4.3.1 Flow Figure to decide Content Provider Type

4.3.1.1. Creating/Using Private Content Providers

Private Content Provider is the Content Provider which is used only in the single application, and the safest Content Provider [14].

[14]However, non-public settings for Content Provider are not functional in Android 2.2 (API Level 8) and previous versions.

Sample code of how to implement a private Content Provider is shown below.

Points (Creating a Content Provider):

  1. Explicitly set the exported attribute to false.
  2. Handle the received request data carefully and securely, even though the data comes from the same application.
  3. Sensitive information can be sent since it is sending and receiving all within the same application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.privateprovider">

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    <activity
        android:name=".PrivateUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    
    <!-- *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <provider
        android:name=".PrivateProvider"
        android:authorities="org.jssec.android.provider.privateprovider"
        android:exported="false" />
  </application>
</manifest>
PrivateProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.privateprovider;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class PrivateProvider extends ContentProvider {

    public static final String AUTHORITY =
            "org.jssec.android.provider.privateprovider";
    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Expose the interface that the Content Provider provides.
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // Since this is a sample program, query method returns the following
    // fixed result always without using database.
    private static MatrixCursor sAddressCursor =
            new MatrixCursor(new String[] { "_id", "city" });
    static {
        sAddressCursor.addRow(new String[] { "1", "New York" });
        sAddressCursor.addRow(new String[] { "2", "Longon" });
        sAddressCursor.addRow(new String[] { "3", "Paris" });
    }
    private static MatrixCursor sDownloadCursor =
            new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {
        // *** POINT 2 *** Handle the received request data carefully and securely,
        // even though the data comes from the same application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        // However, the result of getType rarely has the sensitive meaning.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {

        // *** POINT 2 *** Handle the received request data carefully and securely,
        // even though the data comes from the same application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        // It depends on application whether the query result has sensitive meaning
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // *** POINT 2 *** Handle the received request data carefully and securely,
        // even though the data comes from the same application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        // It depends on application whether the issued ID has sensitive meaning
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {

        // *** POINT 2 *** Handle the received request data carefully and securely,
        // even though the data comes from the same application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        // It depends on application whether the number of updated records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
                return 5;       // Return number of updated records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
                return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // *** POINT 2 *** Handle the received request data carefully and securely,
        // even though the data comes from the same application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** Sensitive information can be sent since it is sending
        // and receiving all within the same application.
        // It depends on application whether the number of deleted records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
                return 10;      // Return number of deleted records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
                return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}

Next is an example of Activity which uses Private Content Provider.

Points (Using a Content Provider):

  1. Sensitive information can be sent since the destination provider is in the same application.
  2. Handle received result data carefully and securely, even though the data comes from the same application.
PrivateUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.privateprovider;

import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PrivateUserActivity extends Activity {

    public void onQueryClick(View view) {

        logLine("[Query]");

        Cursor cursor = null;
        try {
            // *** POINT 4 *** Sensitive information can be sent since the
            // destination provider is in the same application.
            cursor =
                getContentResolver().query(PrivateProvider.Download.CONTENT_URI,
				                          null, null, null, null);

            // *** POINT 5 *** Handle received result data carefully and securely,
            // even though the data comes from the same application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s", cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    public void onInsertClick(View view) {

        logLine("[Insert]");

        // *** POINT 4 *** Sensitive information can be sent since the
        // destination provider is in the same application.
        Uri uri =
            getContentResolver().insert(PrivateProvider.Download.CONTENT_URI,
                                       null);

        // *** POINT 5 *** Handle received result data carefully and securely,
        // even though the data comes from the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine("  uri:" + uri);
    }

    public void onUpdateClick(View view) {

        logLine("[Update]");

        // *** POINT 4 *** Sensitive information can be sent since the
        // destination provider is in the same application.
        int count =
            getContentResolver().update(PrivateProvider.Download.CONTENT_URI,
                                       null, null, null);

        // *** POINT 5 *** Handle received result data carefully and securely,
        // even though the data comes from the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records updated", count));
    }

    public void onDeleteClick(View view) {

        logLine("[Delete]");

        // *** POINT 4 *** Sensitive information can be sent since the
        // destination provider is in the same application.
        int count =
            getContentResolver().delete(PrivateProvider.Download.CONTENT_URI,
                                       null, null);

        // *** POINT 5 *** Handle received result data carefully and securely,
        // even though the data comes from the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records deleted", count));
    }

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.3.1.2. Creating/Using Public Content Providers

Public Content Provider is the Content Provider which is supposed to be used by unspecified large number of applications. It’s necessary to pay attention that since this doesn’t specify clients, it may be attacked and tampered by Malware. For example, a saved data may be taken by select(), a data may be changed by update(), or a fake data may be inserted/deleted by insert()/delete().

In addition, when using a custom Public Content Provider which is not provided by Android OS, it’s necessary to pay attention that request parameter may be received by Malware which masquerades as the custom Public Content Provider, and also the attack result data may be sent. Contacts and MediaStore provided by Android OS are also Public Content Providers, but Malware cannot masquerades as them.

Sample code to implement a Public Content Provider is shown below.

Points (Creating a Content Provider):

  1. Explicitly set the exported attribute to true.
  2. Handle the received request data carefully and securely.
  3. When returning a result, do not include sensitive information.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.publicprovider">

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- *** POINT 1 *** Explicitly set the exported attribute to true. -->
    <provider
        android:name=".PublicProvider"
        android:authorities="org.jssec.android.provider.publicprovider" 
        android:exported="true" />
  </application>
</manifest>
PublicProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.publicprovider;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class PublicProvider extends ContentProvider {

    public static final String AUTHORITY =
            "org.jssec.android.provider.publicprovider";
    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Expose the interface that the Content Provider provides.
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // Since this is a sample program,
    // query method returns the following fixed result always without using
    // database.
    private static MatrixCursor sAddressCursor =
            new MatrixCursor(new String[] { "_id", "city" });
    static {
        sAddressCursor.addRow(new String[] { "1", "New York" });
        sAddressCursor.addRow(new String[] { "2", "London" });
        sAddressCursor.addRow(new String[] { "3", "Paris" });
    }
    private static MatrixCursor sDownloadCursor =
            new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {

        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // *** POINT 2 *** Handle the received request data carefully and securely.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // It depends on application whether the query result has sensitive
        // meaning or not.
        // If no problem when the information is taken by malware, it can be
        // returned as result.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // *** POINT 2 *** Handle the received request data carefully and securely.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // It depends on application whether the issued ID has sensitive
        // meaning or not.
        // If no problem when the information is taken by malware, it can be
        // returned as result.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        // *** POINT 2 *** Handle the received request data carefully and securely.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // It depends on application whether the number of updated records has
        // sensitive meaning or not.
        // If no problem when the information is taken by malware, it can be
        // returned as result.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 5;   // Return number of updated records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // *** POINT 2 *** Handle the received request data carefully and securely.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 3 *** When returning a result, do not include sensitive
        // information.
        // It depends on application whether the number of deleted records has
        // sensitive meaning or not.
        // If no problem when the information is taken by malware, it can be
        // returned as result.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 10;  // Return number of deleted records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}

Next is an example of Activity which uses Public Content Provider.

Points (Using a Content Provider):

  1. Do not send sensitive information.
  2. When receiving a result, handle the result data carefully and securely.
PublicUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.publicuser;

import android.app.Activity;
import android.content.ContentValues;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PublicUserActivity extends Activity {

    // Target Content Provider Information
    private static final String AUTHORITY =
            "org.jssec.android.provider.publicprovider";
    private interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    public void onQueryClick(View view) {

        logLine("[Query]");

        if (!providerExists(Address.CONTENT_URI)) {
            logLine("  Content Provider doesn't exist.");
            return;
        }

        Cursor cursor = null;
        try {
            // *** POINT 4 *** Do not send sensitive information.
            // since the target Content Provider may be malware.
            // If no problem when the information is taken by malware,
            // it can be included in the request.
            cursor = getContentResolver().query(Address.CONTENT_URI,
                                               null, null, null, null);

            // *** POINT 5 *** When receiving a result, handle the result data
            // carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s", cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    public void onInsertClick(View view) {

        logLine("[Insert]");

        if (!providerExists(Address.CONTENT_URI)) {
            logLine("  Content Provider doesn't exist.");
            return;
        }

        // *** POINT 4 *** Do not send sensitive information.
        // since the target Content Provider may be malware.
        // If no problem when the information is taken by malware,
        // it can be included in the request.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);

        // *** POINT 5 *** When receiving a result, handle the result data
        // carefully and securely.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine("  uri:" + uri);
    }

    public void onUpdateClick(View view) {

        logLine("[Update]");

        if (!providerExists(Address.CONTENT_URI)) {
            logLine("  Content Provider doesn't exist.");
            return;
        }

        // *** POINT 4 *** Do not send sensitive information.
        // since the target Content Provider may be malware.
        // If no problem when the information is taken by malware,
        // it can be included in the request.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        String where = "_id = ?";
        String[] args = { "4" };
        int count =
            getContentResolver().update(Address.CONTENT_URI, values, where, args);

        // *** POINT 5 *** When receiving a result, handle the result data
        // carefully and securely.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records updated", count));
    }

    public void onDeleteClick(View view) {

        logLine("[Delete]");

        if (!providerExists(Address.CONTENT_URI)) {
            logLine("  Content Provider doesn't exist.");
            return;
        }

        // *** POINT 4 *** Do not send sensitive information.
        // since the target Content Provider may be malware.
        // If no problem when the information is taken by malware,
        // it can be included in the request.
        int count = getContentResolver().delete(Address.CONTENT_URI, null, null);

        // *** POINT 5 *** When receiving a result, handle the result data
        // carefully and securely.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records deleted", count));
    }

    private boolean providerExists(Uri uri) {
        ProviderInfo pi =
            getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
        return (pi != null);
    }

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.3.1.3. Creating/Using Partner Content Providers

Partner Content Provider is the Content Provider which can be used only by the particular applications. The system consists of a partner company’s application and In-house application, and it is used to protect the information and features which are handled between a partner application and an In-house application.

Sample code to implement a partner-only Content Provider is shown below.

Points (Creating a Content Provider):

  1. Explicitly set the exported attribute to true.
  2. Verify if the certificate of a requesting application has been registered in the own white list.
  3. Handle the received request data carefully and securely, even though the data comes from a partner application.
  4. Information that is granted to disclose to partner applications can be returned.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.partnerprovider">

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <!-- *** POINT 1 *** Explicitly set the exported attribute to true. -->
    <provider
        android:name=".PartnerProvider"
        android:authorities="org.jssec.android.provider.partnerprovider" 
        android:exported="true" />
  </application>
</manifest>
PartnerProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.partnerprovider;

import java.util.List;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;

public class PartnerProvider extends ContentProvider {

    public static final String AUTHORITY =
            "org.jssec.android.provider.partnerprovider";
    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Expose the interface that the Content Provider provides.
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // Since this is a sample program,
    // query method returns the following fixed result always without using
    // database.
    private static MatrixCursor sAddressCursor =
            new MatrixCursor(new String[] { "_id", "city" });
    static {
        sAddressCursor.addRow(new String[] { "1", "New York" });
        sAddressCursor.addRow(new String[] { "2", "London" });
        sAddressCursor.addRow(new String[] { "3", "Paris" });
    }
    private static MatrixCursor sDownloadCursor =
            new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    // *** POINT 2 *** Verify if the certificate of a requesting application has
    // been registered in the own white list.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();

        // Register certificate hash value of partner application
        // org.jssec.android.provider.partneruser.
        sWhitelists.add("org.jssec.android.provider.partneruser", isdebug ?
            // Certificate hash value of "androiddebugkey" in the debug.keystore.
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of "partner key" in the keystore.
            "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");

        // Register following other partner applications in the same way.
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }
    // Get the package name of the calling application.
    private String getCallingPackage(Context context) {
        String pkgname;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            pkgname = super.getCallingPackage();
        } else {
            pkgname = null;
            ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            List<RunningAppProcessInfo> procList = am.getRunningAppProcesses();
            int callingPid = Binder.getCallingPid();
            if (procList != null) {
                for (RunningAppProcessInfo proc : procList) {
                    if (proc.pid == callingPid) {
                        pkgname = proc.pkgList[proc.pkgList.length - 1];
                        break;
                    }
                }
            }
        }
        return pkgname;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {

        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // *** POINT 2 *** Verify if the certificate of a requesting application
        // has been registered in the own white list.
        if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
            throw new SecurityException("Calling application is not a partner application.");
        }

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from a partner application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."

        // *** POINT 4 *** Information that is granted to disclose to partner
        // applications can be returned.
        // It depends on application whether the query result can be disclosed
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // *** POINT 2 *** Verify if the certificate of a requesting application
        // has been registered in the own white list.
        if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
            throw new SecurityException("Calling application is not a partner application.");
        }

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from a partner application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."

        // *** POINT 4 *** Information that is granted to disclose to partner
        // applications can be returned.
        // It depends on application whether the issued ID has sensitive meaning
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        // *** POINT 2 *** Verify if the certificate of a requesting application
        // has been registered in the own white list.
        if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
            throw new SecurityException("Calling application is not a partner application.");
        }

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from a partner application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."

        // *** POINT 4 *** Information that is granted to disclose to partner
        // applications can be returned.
        // It depends on application whether the number of updated records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 5;   // Return number of updated records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // *** POINT 2 *** Verify if the certificate of a requesting application
        // has been registered in the own white list.
        if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
            throw new SecurityException("Calling application is not a partner application.");
        }

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from a partner application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."

        // *** POINT 4 *** Information that is granted to disclose to partner
        // applications can be returned.
        // It depends on application whether the number of deleted records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 10;  // Return number of deleted records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}

Next is an example of Activity which use partner only Content Provider.

Points (Using a Content Provider):

  1. Verify if the certificate of the target application has been registered in the own white list.
  2. Information that is granted to disclose to partner applications can be sent.
  3. Handle the received result data carefully and securely, even though the data comes from a partner application.
PartnerUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.partneruser;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PartnerUserActivity extends Activity {

    // Target Content Provider Information
    private static final String AUTHORITY =
            "org.jssec.android.provider.partnerprovider";
    private interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // *** POINT 4 *** Verify if the certificate of the target application has
    // been registered in the own white list.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();

        // Register certificate hash value of partner application
        // org.jssec.android.provider.partnerprovider.
        sWhitelists.add("org.jssec.android.provider.partnerprovider", isdebug ?
            // Certificate hash value of "androiddebugkey" in the debug.keystore.
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of "partner key" in the keystore.
            "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA");
                
        // Register following other partner applications in the same way.
    }

    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }
        
    // Get package name of target content provider.
    private String providerPkgname(Uri uri) {
        String pkgname = null;
        ProviderInfo pi =
            getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
        if (pi != null) pkgname = pi.packageName;
        return pkgname;
    }

    public void onQueryClick(View view) {

        logLine("[Query]");

        // *** POINT 4 *** Verify if the certificate of the target application has
        // been registered in the own white list.
        if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
            logLine("  The target content provider is not served by partner applications.");
            return;
        }

        Cursor cursor = null;
        try {
            // *** POINT 5 *** Information that is granted to disclose to partner
            // applications can be sent.
            cursor = getContentResolver().query(Address.CONTENT_URI,
                                                null, null, null, null);

            // *** POINT 6 *** Handle the received result data carefully and
            // securely, even though the data comes from a partner application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s", cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    public void onInsertClick(View view) {

        logLine("[Insert]");

        // *** POINT 4 *** Verify if the certificate of the target application has
        // been registered in the own white list.
        if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
            logLine("  The target content provider is not served by partner applications.");
            return;
        }

        // *** POINT 5 *** Information that is granted to disclose to partner
        // applications can be sent.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);

        // *** POINT 6 *** Handle the received result data carefully and securely,
        // even though the data comes from a partner application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine("  uri:" + uri);
    }

    public void onUpdateClick(View view) {

        logLine("[Update]");

        // *** POINT 4 *** Verify if the certificate of the target application has
        // been registered in the own white list.
        if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
            logLine("  The target content provider is not served by partner applications.");
            return;
        }

        // *** POINT 5 *** Information that is granted to disclose to partner
        // applications can be sent.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        String where = "_id = ?";
        String[] args = { "4" };
        int count =
            getContentResolver().update(Address.CONTENT_URI, values, where, args);

        // *** POINT 6 *** Handle the received result data carefully and securely,
        // even though the data comes from a partner application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records updated", count));
    }

    public void onDeleteClick(View view) {

        logLine("[Delete]");

        // *** POINT 4 *** Verify if the certificate of the target application has
        // been registered in the own white list.
        if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
            logLine("  The target content provider is not served by partner applications.");
            return;
        }

        // *** POINT 5 *** Information that is granted to disclose to partner
        // applications can be sent.
        int count = getContentResolver().delete(Address.CONTENT_URI, null, null);

        // *** POINT 6 *** Handle the received result data carefully and securely,
        // even though the data comes from a partner application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records deleted", count));
    }

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

4.3.1.4. Creating/Using In-house Content Providers

In-house Content Provider is the Content Provider which prohibits to be used by applications other than In house only applications.

Sample code of how to implement an In house only Content Provider is shown below.

Points (Creating a Content Provider):

  1. Define an in-house signature permission.
  2. Require the in-house signature permission.
  3. Explicitly set the exported attribute to true.
  4. Verify if the in-house signature permission is defined by an in-house application.
  5. Verify the safety of the parameter even if it’s a request from In house only application.
  6. Sensitive information can be returned since the requesting application is in-house.
  7. When exporting an APK, sign the APK with the same developer key as that of the requesting application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.inhouseprovider">

  <!-- *** POINT 1 *** Define an in-house signature permission -->
  <permission
      android:name="org.jssec.android.provider.inhouseprovider.MY_PERMISSION"
      android:protectionLevel="signature" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >

    <!-- *** POINT 2 *** Require the in-house signature permission -->
    <!-- *** POINT 3 *** Explicitly set the exported attribute to true. -->
    <provider
        android:name=".InhouseProvider"
        android:authorities="org.jssec.android.provider.inhouseprovider"
        android:permission="org.jssec.android.provider.inhouseprovider.MY_PERMISSION" 
        android:exported="true" />
  </application>
</manifest>
InhouseProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.inhouseprovider;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class InhouseProvider extends ContentProvider {

    public static final String AUTHORITY =
            "org.jssec.android.provider.inhouseprovider";
    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Expose the interface that the Content Provider provides.
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // Since this is a sample program, query method returns the following
    // fixed result always without using database.
    private static MatrixCursor sAddressCursor =
            new MatrixCursor(new String[] { "_id", "city" });
    static {
        sAddressCursor.addRow(new String[] { "1", "New York" });
        sAddressCursor.addRow(new String[] { "2", "London" });
        sAddressCursor.addRow(new String[] { "3", "Paris" });
    }
    private static MatrixCursor sDownloadCursor =
            new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.provider.inhouseprovider.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {

        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // *** POINT 4 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
            throw new SecurityException("The in-house signature permission is not declared by in-house application.");
        }

        // *** POINT 5 *** Handle the received request data carefully and securely,
        // even though the data came from an in-house application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 6 *** Sensitive information can be returned since the
        // requesting application is in-house.
        // It depends on application whether the query result has sensitive
        // meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // *** POINT 4 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
            throw new SecurityException("The in-house signature permission is not declared by in-house application.");
        }

        // *** POINT 5 *** Handle the received request data carefully and securely,
        // even though the data came from an in-house application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 6 *** Sensitive information can be returned since the
        // requesting application is in-house.
        // It depends on application whether the issued ID has sensitive meaning
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        // *** POINT 4 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
            throw new SecurityException("The in-house signature permission is not declared by in-house application.");
        }

        // *** POINT 5 *** Handle the received request data carefully and securely,
        // even though the data came from an in-house application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 6 *** Sensitive information can be returned since the
        // requesting application is in-house.
        // It depends on application whether the number of updated records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 5;   // Return number of updated records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // *** POINT 4 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
            throw new SecurityException("The in-house signature permission is not declared by in-house application.");
        }

        // *** POINT 5 *** Handle the received request data carefully and securely,
        // even though the data came from an in-house application.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 6 *** Sensitive information can be returned since the
        // requesting application is in-house.
        // It depends on application whether the number of deleted records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 10;  // Return number of deleted records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 7 *** When exporting an APK, sign the APK with the same developer key as the requesting application.

_images/image35.png

Fig. 4.3.2 Sign the APK with the same developer key as the requesting application

Next is the example of Activity which uses In house only Content Provider.

Point (Using a Content Provider):

  1. Declare to use the in-house signature permission.
  2. Verify if the in-house signature permission is defined by an in-house application.0
  3. Verify if the destination application is signed with the in-house certificate.
  4. Sensitive information can be sent since the destination application is in-house one.
  5. Handle the received result data carefully and securely, even though the data comes from an in-house application.
  6. When exporting an APK, sign the APK with the same developer key as that of the destination application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.jssec.android.provider.inhouseuser">
    
    <!-- *** POINT 8 *** Declare to use the in-house signature permission. -->
    <uses-permission
        android:name="org.jssec.android.provider.inhouseprovider.MY_PERMISSION" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".InhouseUserActivity"
            android:label="@string/app_name"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
InhouseUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.inhouseuser;

import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class InhouseUserActivity extends Activity {

    // Target Content Provider Information
    private static final String AUTHORITY =
            "org.jssec.android.provider.inhouseprovider";
    private interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    // In-house Signature Permission
    private static final String MY_PERMISSION =
            "org.jssec.android.provider.inhouseprovider.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of "androiddebugkey" in the
                // debug.keystore.
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of "my company key" in the keystore.
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    // Get package name of target content provider.
    private static String providerPkgname(Context context, Uri uri) {
        String pkgname = null;
        PackageManager pm = context.getPackageManager();
        ProviderInfo pi = pm.resolveContentProvider(uri.getAuthority(), 0);
        if (pi != null) pkgname = pi.packageName;
        return pkgname;
    }

    public void onQueryClick(View view) {

        logLine("[Query]");

        // *** POINT 9 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            logLine("  The in-house signature permission is not declared by in-house application.");
            return;
        }

        // *** POINT 10 *** Verify if the destination application is signed with
        // the in-house certificate.
        String pkgname = providerPkgname(this, Address.CONTENT_URI);
        if (!PkgCert.test(this, pkgname, myCertHash(this))) {
            logLine("  The target content provider is not served by in-house applications.");
            return;
        }

        Cursor cursor = null;
        try {
            // *** POINT 11 *** Sensitive information can be sent since the
            // destination application is in-house one.
            cursor =
                getContentResolver().query(Address.CONTENT_URI,
                                          null, null, null, null);

            // *** POINT 12 *** Handle the received result data carefully and
            // securely, even though the data comes from an in-house application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s", cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    public void onInsertClick(View view) {

        logLine("[Insert]");

        // *** POINT 9 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        String correctHash = myCertHash(this);
        if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
            logLine("  The in-house signature permission is not declared by in-house application.");
            return;
        }

        // *** POINT 10 *** Verify if the destination application is signed with
        // the in-house certificate.
        String pkgname = providerPkgname(this, Address.CONTENT_URI);
        if (!PkgCert.test(this, pkgname, correctHash)) {
            logLine("  The target content provider is not served by in-house applications.");
            return;
        }

        // *** POINT 11 *** Sensitive information can be sent since the
        // destination application is in-house one.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);

        // *** POINT 12 *** Handle the received result data carefully and securely,
        // even though the data comes from an in-house application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine("  uri:" + uri);
    }

    public void onUpdateClick(View view) {

        logLine("[Update]");

        // *** POINT 9 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        String correctHash = myCertHash(this);
        if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
            logLine("  The in-house signature permission is not declared by in-house application.");
            return;
        }

        // *** POINT 10 *** Verify if the destination application is signed with
        // the in-house certificate.
        String pkgname = providerPkgname(this, Address.CONTENT_URI);
        if (!PkgCert.test(this, pkgname, correctHash)) {
            logLine("  The target content provider is not served by in-house applications.");
            return;
        }

        // *** POINT 11 *** Sensitive information can be sent since the
        // destination application is in-house one.
        ContentValues values = new ContentValues();
        values.put("city", "Tokyo");
        String where = "_id = ?";
        String[] args = { "4" };
        int count =
            getContentResolver().update(Address.CONTENT_URI, values, where, args);

        // *** POINT 12 *** Handle the received result data carefully and securely,
        // even though the data comes from an in-house application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records updated", count));
    }

    public void onDeleteClick(View view) {

        logLine("[Delete]");

        // *** POINT 9 *** Verify if the in-house signature permission is defined
        // by an in-house application.
        String correctHash = myCertHash(this);
        if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
            logLine("  The target content provider is not served by in-house applications.");
            return;
        }

        // *** POINT 10 *** Verify if the destination application is signed with
        // the in-house certificate.
        String pkgname = providerPkgname(this, Address.CONTENT_URI);
        if (!PkgCert.test(this, pkgname, correctHash)) {
            logLine("  The target content provider is not served by in-house applications.");
            return;
        }

        // *** POINT 11 *** Sensitive information can be sent since the
        // destination application is in-house one.
        int count = getContentResolver().delete(Address.CONTENT_URI, null, null);

        // *** POINT 12 *** Handle the received result data carefully and securely,
        // even though the data comes from an in-house application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        logLine(String.format("  %s records deleted", count));
    }

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 13 *** When exporting an APK, sign the APK with the same developer key as that of the destination application.

_images/image35.png

Fig. 4.3.3 Sign the APK with the same developer key as the destination application

4.3.1.5. Creating/Using Temporary permit Content Providers

Temporary permit Content Provider is basically a private Content Provider, but this permits the particular applications to access the particular URI. By sending an Intent which special flag is specified to the target applications, temporary access permission is provided to those applications. Contents provider side application can give the access permission actively to other applications, and it can also give access permission passively to the application which claims the temporary access permission.

Sample code of how to implement a temporary permit Content Provider is shown below.

Points (Creating a Content Provider):

  1. Explicitly set the exported attribute to false.
  2. Specify the path to grant access temporarily with the grant-uri-permission.
  3. Handle the received request data carefully and securely, even though the data comes from the application granted access temporarily.
  4. Information that is granted to disclose to the temporary access applications can be returned.
  5. Specify URI for the intent to grant temporary access.
  6. Specify access rights for the intent to grant temporary access.
  7. Send the explicit intent to an application to grant temporary access.
  8. Return the intent to the application that requests temporary access.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.temporaryprovider">

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >

    <activity
        android:name=".TemporaryActiveGrantActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <!-- Temporary Content Provider -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <provider
        android:name=".TemporaryProvider"
        android:authorities="org.jssec.android.provider.temporaryprovider"
        android:exported="false" >

      <!-- *** POINT 2 *** Specify the path to grant access temporarily with the grant-uri-permission. -->
      <grant-uri-permission android:path="/addresses" />

    </provider>

    <activity
        android:name=".TemporaryPassiveGrantActivity"
        android:label="@string/app_name"
        android:exported="true" />
  </application>
</manifest>
TemporaryProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.temporaryprovider;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class TemporaryProvider extends ContentProvider {
    public static final String AUTHORITIY =
            "org.jssec.android.provider.temporaryprovider";
    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Expose the interface that the Content Provider provides.
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITIY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITIY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITIY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITIY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITIY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITIY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // Since this is a sample program,
    // query method returns the following fixed result always without using
    // database.
    private static MatrixCursor sAddressCursor =
            new MatrixCursor(new String[] { "_id", "city" });
    static {
        sAddressCursor.addRow(new String[] { "1", "New York" });
        sAddressCursor.addRow(new String[] { "2", "London" });
        sAddressCursor.addRow(new String[] { "3", "Paris" });
    }
    private static MatrixCursor sDownloadCursor =
            new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {

        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from the application granted access
        // temporarily.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 4 *** Information that is granted to disclose to the
        // temporary access applications can be returned.
        // It depends on application whether the query result can be disclosed
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from the application granted access
        // temporarily.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 4 *** Information that is granted to disclose to the
        // temporary access applications can be returned.
        // It depends on application whether the issued ID has sensitive meaning
        // or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from the application granted access
        // temporarily.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 4 *** Information that is granted to disclose to the
        // temporary access applications can be returned.
        // It depends on application whether the number of updated records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 5;   // Return number of updated records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // *** POINT 3 *** Handle the received request data carefully and securely,
        // even though the data comes from the application granted access
        // temporarily.
        // Here, whether uri is within expectations or not, is verified by
        // UriMatcher#match() and switch case.
        // Checking for other parameters are omitted here, due to sample.
        // Please refer to "3.2 Handle Input Data Carefully and Securely."
        
        // *** POINT 4 *** Information that is granted to disclose to the
        // temporary access applications can be returned.
        // It depends on application whether the number of deleted records has
        // sensitive meaning or not.
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 10;  // Return number of deleted records

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}
TemporaryActiveGrantActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.temporaryprovider;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class TemporaryActiveGrantActivity extends Activity {

    // User Activity Information
    private static final String TARGET_PACKAGE =
            "org.jssec.android.provider.temporaryuser";
    private static final String TARGET_ACTIVITY =
            "org.jssec.android.provider.temporaryuser.TemporaryUserActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.active_grant);
    }

    // In the case that Content Provider application grants access permission to
    // other application actively.
    public void onSendClick(View view) {
        try {
            Intent intent = new Intent();

            // *** POINT 5 *** Specify URI for the intent to grant temporary
            // access.
            intent.setData(TemporaryProvider.Address.CONTENT_URI);

            // *** POINT 6 *** Specify access rights for the intent to grant
            // temporary access.
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

            // *** POINT 7 *** Send the explicit intent to an application to grant
            // temporary access.
            intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
            startActivity(intent);

        } catch (ActivityNotFoundException e) {
            Toast.makeText(this,
                           "User Activity not found.", Toast.LENGTH_LONG).show();
        }
    }
}
TemporaryPassiveGrantActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.temporaryprovider;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class TemporaryPassiveGrantActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.passive_grant);
    }

    // In the case that Content Provider application passively grants access
    // permission to the application that requested Content Provider access.
    public void onGrantClick(View view) {
        Intent intent = new Intent();

        // *** POINT 5 *** Specify URI for the intent to grant temporary access.
        intent.setData(TemporaryProvider.Address.CONTENT_URI);

        // *** POINT 6 *** Specify access rights for the intent to grant temporary
        // access.
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        // *** POINT 8 *** Return the intent to the application that requests
        // temporary access.
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    public void onCloseClick(View view) {
        finish();
    }
}

Next is the example of temporary permit Content Provider.

Points (Using a Content Provider):

  1. Do not send sensitive information.
  2. When receiving a result, handle the result data carefully and securely.
TemporaryUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.provider.temporaryuser;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class TemporaryUserActivity extends Activity {

    // Information of the Content Provider's Activity to request temporary content
    // provider access.
    private static final String TARGET_PACKAGE =
        "org.jssec.android.provider.temporaryprovider";
    private static final String TARGET_ACTIVITY =
        "org.jssec.android.provider.temporaryprovider.TemporaryPassiveGrantActivity";

    // Target Content Provider Information
    private static final String AUTHORITY =
        "org.jssec.android.provider.temporaryprovider";
    private interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    private static final int REQUEST_CODE = 1;

    public void onQueryClick(View view) {

        logLine("[Query]");

        Cursor cursor = null;
        try {
            if (!providerExists(Address.CONTENT_URI)) {
                logLine("  Content Provider doesn't exist.");
                return;
            }

            // *** POINT 9 *** Do not send sensitive information.
            // If no problem when the information is taken by malware, it can be
            // included in the request.
            cursor = getContentResolver().query(Address.CONTENT_URI,
                                               null, null, null, null);

            // *** POINT 10 *** When receiving a result, handle the result data
            // carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s", cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        } catch (SecurityException ex) {
            logLine("  Exception:" + ex.getMessage());
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    // In the case that this application requests temporary access to the Content
    // Provider and the Content Provider passively grants temporary access
    // permission to this application.
    public void onGrantRequestClick(View view) {
        Intent intent = new Intent();
        intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
        try {
            startActivityForResult(intent, REQUEST_CODE);
        } catch (ActivityNotFoundException e) {
            logLine("Content Provider's Activity not found.");
        }
    }

    private boolean providerExists(Uri uri) {
        ProviderInfo pi =
            getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
        return (pi != null);
    }

    private TextView mLogView;

    // In the case that the Content Provider application grants temporary access
    // to this application actively.
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.3.2. Rule Book

Be sure to follow the rules below when Implementing or using a content provider.

  1. Content Provider that Is Used Only in an Application Must Be Set as Private (Required)
  2. Handle the Received Request Parameter Carefully and Securely (Required)
  3. Use an In-house Defined Signature Permission after Verifying that it is Defined by an In-house Application (Required)
  4. When Returning a Result, Pay Attention to the Possibility of Information Leakage of that Result from the Destination Application (Required)
  5. When Providing an Asset Secondarily, the Asset should be Protected with the Same Level of Protection (Required)

And user side should follow the below rules, too.

  1. Handle the Returned Result Data from the Content Provider Carefully and Securely (Required)

4.3.2.1. Content Provider that Is Used Only in an Application Must Be Set as Private (Required)

Content Provider which is used only in a single application is not necessary to be accessed by other applications, and the access which attacks the Content Provider is not often considered by developers. A Content Provider is basically the system to share data, so it’s handled as public by default. A Content Provider which is used only in a single application should be set as private explicitly, and it should be a private Content Provider. In Android 2.3.1 (API Level 9) or later, a Content Provider can be set as private by specifying android:exported=”false” in provider element.

AndroidManifest.xml
    <!-- 4.3.1.1 - *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <provider
        android:name=".PrivateProvider"
        android:authorities="org.jssec.android.provider.privateprovider"
        android:exported="false" />

4.3.2.2. Handle the Received Request Parameter Carefully and Securely (Required)

Risks differ depending on the types of Content Providers, but when processing request parameters, the first thing you should do is input validation.

Although each method of a Content Provider has the interface which is supposed to receive the component parameter of SQL statement, actually it simply hands over the arbitrary character string in the system, so it’s necessary to pay attention that Contents Provider side needs to suppose the case that unexpected parameter may be provided.

Since Public Content Providers can receive requests from untrusted sources, they can be attacked by malware. On the other hand, Private Content Providers will never receive any requests from other applications directly, but it is possible that a Public Activity in the targeted application may forward a malicious Intent to a Private Content Provider so you should not assume that Private Content Providers cannot receive any malicious input.

Since other Content Providers also have the risk of a malicious intent being forwarded to them as well, it is necessary to perform input validation on these requests as well.

Please refer to “3.2. Handling Input Data Carefully and Securely”.

4.3.2.3. Use an In-house Defined Signature Permission after Verifying that it is Defined by an In-house Application (Required)

Make sure to protect your in-house Content Providers by defining an in-house signature permission when creating the Content Provider. Since defining a permission in the AndroidManifest.xml file or declaring a permission request does not provide adequate security, please be sure to refer to “5.2.1.2. How to Communicate Between In-house Applications with In-house-defined Signature Permission.”

4.3.2.4. When Returning a Result, Pay Attention to the Possibility of Information Leakage of that Result from the Destination Application (Required)

In case of query() or insert(), Cursor or Uri is returned to the request sending application as a result information. When sensitive information is included in the result information, the information may be leaked from the destination application. In case of update() or delete(), number of updated/deleted records is returned to the request sending application as a result information. In rare cases, depending on some application specs, the number of updated/deleted records has the sensitive meaning, so please pay attention to this.

4.3.2.5. When Providing an Asset Secondarily, the Asset should be Protected with the Same Level of Protection (Required)

When an information or function asset, which is protected by a permission, is provided to another application secondhand, you need to make sure that it has the same required permissions needed to access the asset. In the Android OS permission security model, only an application that has been granted proper permissions can directly access a protected asset. However, there is a loophole because an application with permissions to an asset can act as a proxy and allow access to an unprivileged application. Substantially this is the same as re-delegating a permission, so it is referred to as the “Permission Re-delegation” problem. Please refer to “5.2.3.4. Permission Re-delegation Problem.”

4.3.2.6. Handle the Returned Result Data from the Content Provider Carefully and Securely (Required)

Risks differ depending on the types of Content Provider, but when processing a result data, the first thing you should do is input validation.

In case that the destination Content Provider is a public Content Provider, Malware which masquerades as the public Content Provider may return the attack result data. On the other hand, in case that the destination Content Provider is a private Content Provider, it is less risk because it receives the result data from the same application, but you should not assume that private Content Providers cannot receive any malicious input. Since other Content Providers also have the risk of a malicious data being returned to them as well, it is necessary to perform input validation on that result data as well.

Please refer to “3.2. Handling Input Data Carefully and Securely

4.4. Creating/Using Services

4.4.1. Sample Code

The risks and countermeasures of using Services differ depending on how that Service is being used. You can find out which type of Service you are supposed to create through the following chart shown below. Since the secure coding best practice varies according to how the service is created, we will also explain about the implementation of the Service as well.

Table 4.4.1 Definition of service types
Type Definition
Private Service A service that cannot be used another application, and therefore is the safest service.
Public Service A service that is supposed to be used by an unspecified large number of applications
Partner Service A service that can only be used by the specific applications made by a trusted partner company.
In-house Service A service that can only be used by other in-house applications.
_images/image44.png

Fig. 4.4.1 Flow Figure to select Service Type

There are several implementation methods for Service, and you will select the method which matches with the type of Service that you suppose to create. The items of vertical columns in the table show the implementation methods, and these are divided into 5 types. “OK” stands for the possible combination and others show impossible/difficult combinations in the table.

Please refer to “4.4.3.2. How to Implement Service” and Sample code of each Service type (with * mark in a table) for detailed implementation methods of Service.

Table 4.4.2 Implementation Methods of Service
Category Private Service Public Service Partner Service In-house Service
startService type OK* OK - OK
IntentService type OK OK* - OK
local bind type OK - - -
Messenger bind type OK OK - OK*
AIDL bind type OK OK OK* OK

Sample code for each security type of Service are shown as below, by using combination of * mark in Table 4.4.2.

4.4.1.1. Creating/Using Private Services

Private Services are Services which cannot be launched by the other applications and therefore it is the safest Service.

When using Private Services that are only used within the application, as long as you use explicit Intents to the class then you do not have to worry about accidently sending it to any other application.

Sample code of how to use the startService type Service is shown below.

Points (Creating a Service):

  1. Explicitly set the exported attribute to false.
  2. Handle the received intent carefully and securely, even though the intent was sent from the same application.
  3. Sensitive information can be sent since the requesting application is in the same application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.privateservice" >

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name=".PrivateUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    
    <!-- Private Service derived from Service class -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <service android:name=".PrivateStartService" android:exported="false"/>
    
    <!-- Private Service derived from IntentService class -->
    <!-- *** POINT 1 *** Explicitly set the exported attribute to false. -->
    <service android:name=".PrivateIntentService" android:exported="false"/>
    
  </application>

</manifest>
PrivateStartService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.privateservice;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.widget.Toast;

public class PrivateStartService extends Service {
        
    // The onCreate gets called only one time when the service starts.
    @Override
    public void onCreate() {
        Toast.makeText(this, "PrivateStartService - onCreate()",
                       Toast.LENGTH_SHORT).show();
    }

    // The onStartCommand gets called each time after the startService gets called.
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // *** POINT 2 *** Handle the received intent carefully and securely,
        // even though the intent was sent from the same application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = intent.getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("PrivateStartService\nReceived param: \"%s\"",
                                     param),
                       Toast.LENGTH_LONG).show();

        return Service.START_NOT_STICKY;
    }

    // The onDestroy gets called only one time when the service stops.
    @Override
    public void onDestroy() {
        Toast.makeText(this,
                       "PrivateStartService - onDestroy()",
                        Toast.LENGTH_SHORT).show();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // This service does not provide binding, so return null
        return null;
    }
}

Next is sample code for Activity which uses Private Service.

Points (Using a Service):

  1. Use the explicit intent with class specified to call a service in the same application.
  2. Sensitive information can be sent since the destination service is in the same application.
  3. Handle the received result data carefully and securely, even though the data came from a service in the same application.
PrivateUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.privateservice;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class PrivateUserActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.privateservice_activity);
    }

    // --- StartService control ---
        
    public void onStartServiceClick(View v) {
        // *** POINT 4 *** Use the explicit intent with class specified to call
        // a service in the same application.
        Intent intent = new Intent(this, PrivateStartService.class);
        
        // *** POINT 5 *** Sensitive information can be sent since the destination
        // service is in the same application.
        intent.putExtra("PARAM", "Sensitive information");
        
        startService(intent);
    }

    // -- StopService control --

    public void onStopServiceClick(View v) {
        doStopService();
    }

    @Override
    public void onStop() {
        super.onStop();
        // Stop service if the service is running.
        doStopService();
    }

    private void doStopService() {
        // *** POINT 4 *** Use the explicit intent with class specified to call
        // a service in the same application.
        Intent intent = new Intent(this, PrivateStartService.class);
        stopService(intent);
    }

    // --- IntentService control ---

    public void onIntentServiceClick(View v) {
        // *** POINT 4 *** Use the explicit intent with class specified to call
        // a service in the same application.
        Intent intent = new Intent(this, PrivateIntentService.class);
          
        // *** POINT 5 *** Sensitive information can be sent since the destination
        // service is in the same application.
        intent.putExtra("PARAM", "Sensitive information");

        startService(intent);
    }
}

4.4.1.2. Creating/Using Public Services

Public Service is the Service which is supposed to be used by the unspecified large number of applications. It’s necessary to pay attention that it may receive the information (Intent etc.) which was sent by Malware. In addition, since an Intent to start Service may be received by Malware, explicit Intent should be used for launching Public Service, and <intent-filter> should not be declared in Service.

Sample code of how to use the startService type Service is shown below.

Points (Creating a Service):

  1. Explicitly set exported = “true” without defining the intent filter.
  2. Handle the received intent carefully and securely.
  3. When returning a result, do not include sensitive information.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.publicservice" >

  <!-- API 28 -->
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    
    <!-- Most standard Service -->
    <!-- *** POINT 1 *** Explicitly set exported = "true" without defining the intent filter. -->
    <service android:name=".PublicStartService" android:exported="true" />

    <!-- Public Service derived from IntentService class -->
    <!-- *** POINT 1 *** Explicitly set exported = "true" without defining the intent filter. -->
    <service android:name=".PublicIntentService" android:exported="true" />

  </application>

</manifest>
PublicIntentService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.publicservice;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.widget.Toast;

public class PublicIntentService extends IntentService{

    public static final String INTENT_CHANNEL = "intent_channel";

    /**
     * Default constructor must be provided when a service extends
     * IntentService class.
     * If it does not exist, an error occurs.
     */
    public PublicIntentService() {
        super("CreatingTypeBService");
    }

    // The onCreate gets called only one time when the Service starts.
    @Override
    public void onCreate() {
        super.onCreate();
        
        Toast.makeText(this,
                       this.getClass().getSimpleName() + " - onCreate()",
                       Toast.LENGTH_SHORT).show();
    }
    
    // The onHandleIntent gets called each time after the startService gets called.
    @Override
    protected void onHandleIntent(Intent intent) {
        if (Build.VERSION.SDK_INT >= 26) {
            Context context = getApplicationContext();
            String title = context.getString(R.string.app_name);
            NotificationChannel default_channel =
                new NotificationChannel(INTENT_CHANNEL, "Intent Channel",
                                        NotificationManager.IMPORTANCE_DEFAULT);
            NotificationManager notificationManager =
                (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.createNotificationChannel(default_channel);
            Notification notification =
                new Notification.Builder(context, INTENT_CHANNEL)
                    .setContentTitle(title)
                    .setSmallIcon(android.R.drawable.btn_default)
                    .setContentText("Intent Channel")
                    .setAutoCancel(true)
                    .setWhen(System.currentTimeMillis())
                    .build();
            startForeground(1, notification);
        }
        // *** POINT 2 *** Handle intent carefully and securely.
        // Since it's public service, the intent may come from malicious
        // application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = intent.getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Recieved parameter \"%s\"", param),
                       Toast.LENGTH_LONG).show();
    }


    // The onDestroy gets called only one time when the service stops.
    @Override
    public void onDestroy() {
        Toast.makeText(this,
                       this.getClass().getSimpleName() + " - onDestroy()",
                       Toast.LENGTH_SHORT).show();
    }
    
}

Next is sample code for Activity which uses Public Service.

Points (Using a Service):

  1. Call service by Explicit Intent
  2. Do not send sensitive information.
  3. When receiving a result, handle the result data carefully and securely.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.publicserviceuser" >

  <queries>
    <package android:name="org.jssec.android.service.publicservice" />
  </queries>
  
  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name=".PublicUserActivity"
        android:label="@string/app_name"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    
  </application>

</manifest>
PublicUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.publicserviceuser;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

public class PublicUserActivity extends Activity {

    // Using Service Info
    private static final String TARGET_PACKAGE =
            "org.jssec.android.service.publicservice";
    private static final String TARGET_START_CLASS =
            "org.jssec.android.service.publicservice.PublicStartService";
    private static final String TARGET_INTENT_CLASS =
            "org.jssec.android.service.publicservice.PublicIntentService";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.publicservice_activity);
    }
    
    // --- StartService control ---
    
    public void onStartServiceClick(View v) {              
        Intent intent = new Intent("org.jssec.android.service.publicservice.action.startservice");

        // *** POINT 4 *** Call service by Explicit Intent
        intent.setClassName(TARGET_PACKAGE, TARGET_START_CLASS);

        // *** POINT 5 *** Do not send sensitive information.
        intent.putExtra("PARAM", "Not sensitive information");

        if (Build.VERSION.SDK_INT >= 26) {
            startForegroundService(intent);
        } else {
            startService(intent);
        }

        startService(intent);
        // *** POINT 6 *** When receiving a result, handle the result data
        // carefully and securely.
        // This sample code uses startService(), so receiving no result.
    }
    
    // --- StopService control ---

    public void onStopServiceClick(View v) {
        doStopService();
    }
        
    // --- IntentService control ---

    public void onIntentServiceClick(View v) {      
        Intent intent = new Intent("org.jssec.android.service.publicservice.action.intentservice");

        // *** POINT 4 *** Call service by Explicit Intent
        intent.setClassName(TARGET_PACKAGE, TARGET_INTENT_CLASS);

        // *** POINT 5 *** Do not send sensitive information.
        intent.putExtra("PARAM", "Not sensitive information");

        if (Build.VERSION.SDK_INT >= 26) {
            startForegroundService(intent);
        } else {
            startService(intent);
        }
    }
        
    @Override
    public void onStop(){
        super.onStop();
        // Stop service if the service is running.
        doStopService();
    }
    
    // Stop service
    private void doStopService() {            
        Intent intent = new Intent("org.jssec.android.service.publicservice.action.startservice");

        // *** POINT 4 *** Call service by Explicit Intent
        intent.setClassName(TARGET_PACKAGE, TARGET_START_CLASS);

        stopService(intent);            
    }
}

4.4.1.3. Creating/Using Partner Services

Partner Service is Service which can be used only by the particular applications. System consists of partner company’s application and In house application, this is used to protect the information and features which are handled between a partner application and In house application.

Following is an example of AIDL bind type Service.

Points (Creating a Service):

  1. Explicitly set exported = “true” without defining the intent filter.
  2. Verify that the certificate of the requesting application has been registered in the own white list.
  3. Do not (Cannot) recognize whether the requesting application is partner or not by onBind (onStartCommand, onHandleIntent).
  4. Handle the received intent carefully and securely, even though the intent was sent from a partner application.
  5. Return only information that is granted to be disclosed to a partner application.

In addition, refer to “5.2.1.3. How to Verify the Hash Value of an Application’s Certificate” for how to verify the certification hash value of destination application which is specified to white list.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.partnerservice.aidl" >
  
  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    
    <!-- Service using AIDL -->
    <!-- *** POINT 1 *** Explicitly set exported = "true" without defining the intent filter. -->
    <service 
        android:name="org.jssec.android.service.partnerservice.aidl.PartnerAIDLService" 
        android:exported="true" />
  </application>

</manifest>

In this example, 2 AIDL files are to be created. One is for callback interface to give data from Service to Activity. The other one is Interface to give data from Activity to Service and to get information. In addition, package name that is described in AIDL file should be consistent with directory hierarchy in which AIDL file is created, same like package name described in java file.

IPartnerAIDLServiceCallback.aidl
package org.jssec.android.service.partnerservice.aidl;

interface IPartnerAIDLServiceCallback {
    /**
     * It's called when the value is changed.
     */
    void valueChanged(String info);
}
IPartnerAIDLService.aidl
package org.jssec.android.service.partnerservice.aidl;

import org.jssec.android.service.partnerservice.aidl.IExclusiveAIDLServiceCallback;

interface IPartnerAIDLService {

    /**
     * Register Callback
     */
    void registerCallback(IPartnerAIDLServiceCallback cb);

    /**
     * Get Information
     */
    String getInfo(String param);

    /**
     * Unregister Callback
     */
    void unregisterCallback(IPartnerAIDLServiceCallback cb);
}
PartnerAIDLService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.partnerservice.aidl;

import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.widget.Toast;

public class PartnerAIDLService extends Service {
    private static final int REPORT_MSG = 1;
    private static final int GETINFO_MSG = 2;

    // The value which this service informs to client
    private int mValue = 0;

    // *** POINT 2 *** Verify that the certificate of the requesting application
    // has been registered in the own white list.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();

        // Register certificate hash value of partner application
        // "org.jssec.android.service.partnerservice.aidluser"
        sWhitelists.add("org.jssec.android.service.partnerservice.aidluser", isdebug ?
            // Certificate hash value of  debug.keystore "androiddebugkey"
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of keystore "partner key"
            "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");

        // Register other partner applications in the same way
    }

    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }

    // Object to register callback
    // Methods which RemoteCallbackList provides are thread-safe.
    private final RemoteCallbackList<IPartnerAIDLServiceCallback> mCallbacks =
        new RemoteCallbackList<IPartnerAIDLServiceCallback>();

    // Handler to send data when callback is called.
    private static class ServiceHandler extends Handler{

        private Context mContext;
        private RemoteCallbackList<IPartnerAIDLServiceCallback> mCallbacks;
        private int mValue = 0;

        public ServiceHandler(Context context,
            RemoteCallbackList<IPartnerAIDLServiceCallback> callback, int value){
            this.mContext = context;
            this.mCallbacks = callback;
            this.mValue = value;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case REPORT_MSG: {
                if(mCallbacks == null){
                    return;
                }
                // Start broadcast
                // To call back on to the registered clients, use beginBroadcast().
                // beginBroadcast() makes a copy of the currently registered
                // callback list.
                final int N = mCallbacks.beginBroadcast();
                for (int i = 0; i < N; i++) {
                    IPartnerAIDLServiceCallback target =
                            mCallbacks.getBroadcastItem(i);
                    try {
                        // *** POINT 5 *** Information that is granted to disclose
                        // to partner applications can be returned.
                        target.valueChanged("Information disclosed to partner application (callback from Service) No." + (++mValue));

                    } catch (RemoteException e) {
                        // Callbacks are managed by RemoteCallbackList, do not
                        // unregister callbacks here.
                        // RemoteCallbackList.kill() unregister all callbacks
                    }
                }
                // finishBroadcast() cleans up the state of a broadcast previously
                // initiated by calling beginBroadcast().
                mCallbacks.finishBroadcast();

                // Repeat after 10 seconds
                sendEmptyMessageDelayed(REPORT_MSG, 10000);
                break;
            }
            case GETINFO_MSG: {
                if(mContext != null) {
                    Toast.makeText(mContext,
                                   (String) msg.obj, Toast.LENGTH_LONG).show();
                }
                break;
            }
            default:
                super.handleMessage(msg);
                break;
            } // switch
        }
    }

    protected final ServiceHandler mHandler =
            new ServiceHandler(this, mCallbacks, mValue);

    // Interfaces defined in AIDL
    private final IPartnerAIDLService.Stub mBinder =
        new IPartnerAIDLService.Stub() {
            private boolean checkPartner() {
                Context ctx = PartnerAIDLService.this;
                if (!PartnerAIDLService.checkPartner(ctx,
                        Utils.getPackageNameFromUid(ctx, getCallingUid()))) {
                    mHandler.post(new Runnable(){
                        @Override
                        public void run(){
                            Toast.makeText(PartnerAIDLService.this,
                                "Requesting application is not partner application.",
                                Toast.LENGTH_LONG).show();
                        }
                    });
                    return false;
                }
                return true;
            }
            public void registerCallback(IPartnerAIDLServiceCallback cb) {
                // *** POINT 2 *** Verify that the certificate of the requesting
                // application has been registered in the own white list.
                if (!checkPartner()) {
                    return;
                }
                if (cb != null) mCallbacks.register(cb);
            }
            public String getInfo(String param) {
                // *** POINT 2 *** Verify that the certificate of the requesting
                // application has been registered in the own white list.
                if (!checkPartner()) {
                    return null;
                }
                // *** POINT 4 *** Handle the received intent carefully and
                // securely, even though the intent was sent from a partner
                // application
                // Omitted, since this is a sample. Please refer to
                // "3.2 Handling Input Data Carefully and Securely."
                Message msg = new Message();
                msg.what = GETINFO_MSG;
                msg.obj = String.format("Method calling from partner application. Recieved \"%s\"", param);
                PartnerAIDLService.this.mHandler.sendMessage(msg);

                // *** POINT 5 *** Return only information that is granted to be
                // disclosed to a partner application.
                return "Information disclosed to partner application (method from Service)";
            }

            public void unregisterCallback(IPartnerAIDLServiceCallback cb) {
                // *** POINT 2 *** Verify that the certificate of the requesting
                // application has been registered in the own white list.
                if (!checkPartner()) {
                    return;
                }

                if (cb != null) mCallbacks.unregister(cb);
            }
        };

    @Override
    public IBinder onBind(Intent intent) {
        // *** POINT 3 *** Verify that the certificate of the requesting
        // application has been registered in the own white list.
        // So requesting application must be validated in methods defined
        // in AIDL every time.
        return mBinder;
    }

    @Override
    public void onCreate() {
        Toast.makeText(this,
                       this.getClass().getSimpleName() + " - onCreate()",
                       Toast.LENGTH_SHORT).show();

        // During service is running, inform the incremented number periodically.
        mHandler.sendEmptyMessage(REPORT_MSG);
    }

    @Override
    public void onDestroy() {
        Toast.makeText(this,
                       this.getClass().getSimpleName() + " - onDestroy()",
                       Toast.LENGTH_SHORT).show();

        // Unregister all callbacks
        mCallbacks.kill();

        mHandler.removeMessages(REPORT_MSG);
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

Next is sample code of Activity which uses partner only Service.

Points (Using a Service):

  1. Verify if the certificate of the target application has been registered in the own white list.
  2. Return only information that is granted to be disclosed to a partner application.
  3. Use the explicit intent to call a partner service.
  4. Handle the received result data carefully and securely, even though the data came from a partner application.
PartnerAIDLUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.partnerservice.aidluser;

import org.jssec.android.service.partnerservice.aidl.IPartnerAIDLService;
import org.jssec.android.service.partnerservice.aidl.IPartnerAIDLServiceCallback;
import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.view.View;
import android.widget.Toast;

public class PartnerAIDLUserActivity extends Activity {

    private boolean mIsBound;
    private Context mContext;
    
    private final static int MGS_VALUE_CHANGED = 1;
    
    // *** POINT 6 *** Verify if the certificate of the target application has
    // been registered in the own white list.
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();
                
        // Register certificate hash value of partner service application
        // "org.jssec.android.service.partnerservice.aidl"
        sWhitelists.add("org.jssec.android.service.partnerservice.aidl", isdebug ?
            // Certificate hash value of debug.keystore "androiddebugkey"
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
            // Certificate hash value of  keystore "my company key"
            "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA");
                
        // Register other partner service applications in the same way
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }

    // Information about destination (requested) partner activity.
    private static final String TARGET_PACKAGE =
            "org.jssec.android.service.partnerservice.aidl";
    private static final String TARGET_CLASS =
            "org.jssec.android.service.partnerservice.aidl.PartnerAIDLService";

    private static class ReceiveHandler extends Handler{

        private Context mContext;

        public ReceiveHandler(Context context){
            this.mContext = context;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MGS_VALUE_CHANGED: {
                String info = (String)msg.obj;
                Toast.makeText(mContext,
                        String.format("Received \"%s\" with callback.", info),
                        Toast.LENGTH_SHORT).show();
                break;
            }
            default:
                super.handleMessage(msg);
                break;
            } // switch
        }    
    }

    private final ReceiveHandler mHandler = new ReceiveHandler(this);
    
    // Interfaces defined in AIDL. Receive notice from service
    private final IPartnerAIDLServiceCallback.Stub mCallback =
        new IPartnerAIDLServiceCallback.Stub() {
            @Override
            public void valueChanged(String info) throws RemoteException {
                Message msg = mHandler.obtainMessage(MGS_VALUE_CHANGED, info);
                mHandler.sendMessage(msg);
            }
        };
    
    // Interfaces defined in AIDL. Inform service.
    private IPartnerAIDLService mService = null;
    
    // Connection used to connect with service. This is necessary when service is
    // implemented with bindService().
    private ServiceConnection mConnection = new ServiceConnection() {

            // This is called when the connection with the service has been
            // established.
            @Override
            public void onServiceConnected(ComponentName className,
                                           IBinder service) {
                mService = IPartnerAIDLService.Stub.asInterface(service);
            
                try{
                    // connect to service
                    mService.registerCallback(mCallback);
                
                }catch(RemoteException e){
                    // service stopped abnormally
                }
            
                Toast.makeText(mContext,
                               "Connected to service",
                               Toast.LENGTH_SHORT).show();
            }

            // This is called when the service stopped abnormally and connection
            // is disconnected.
            @Override
            public void onServiceDisconnected(ComponentName className) {
                Toast.makeText(mContext,
                               "Disconnected from service",
                               Toast.LENGTH_SHORT).show();
            }
        };
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.partnerservice_activity);

        mContext = this;
    }

    // -- StartService control --
    public void onStartServiceClick(View v) {
        // Start bindService
        doBindService();
    }

    // -- GetInfo control --
    public void onGetInfoClick(View v) {
        getServiceinfo();
    }

    // -- StopService control --
    public void onStopServiceClick(View v) {
        doUnbindService();
    }
        
    @Override
    public void onDestroy() {
        super.onDestroy();
        doUnbindService();
    }

    /**
     * Connect to service
     */
    private void doBindService() {
        if (!mIsBound){
            // *** POINT 6 *** Verify if the certificate of the target application
            // has been registered in the own white list.
            if (!checkPartner(this, TARGET_PACKAGE)) {
                Toast.makeText(this,
                    "Destination(Requested) sevice application is not registered in white list.", Toast.LENGTH_LONG).show();
                return;
            }
            
            Intent intent = new Intent();
                
            // *** POINT 7 *** Return only information that is granted to be
            // disclosed to a partner application.
            intent.putExtra("PARAM",
                            "Information disclosed to partner application");
                
            // *** POINT 8 *** Use the explicit intent to call a partner service.
            intent.setClassName(TARGET_PACKAGE, TARGET_CLASS);
                 
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
        }
    }

    /**
     * Disconnect service
     */
    private void doUnbindService() {
        if (mIsBound) {
            // Unregister callbacks which have been registered. 
            if(mService != null){
                try{
                    mService.unregisterCallback(mCallback);
                }catch(RemoteException e){
                    // Service stopped abnormally
                    // Omitted, since it' s sample.
                }
            }
            
            unbindService(mConnection);
            
            Intent intent = new Intent();
        
            // *** POINT 8 *** Use the explicit intent to call a partner service.
            intent.setClassName(TARGET_PACKAGE, TARGET_CLASS);
 
            stopService(intent);
          
            mIsBound = false;
        }
    }

    /**
     * Get information from service
     */
    void getServiceinfo() {
        if (mIsBound && mService != null) {
            String info = null;
            
            try {
                // *** POINT 7 *** Return only information that is granted to be
                // disclosed to a partner application.
                info = mService.getInfo("Information disclosed to partner application (method from activity)");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            // *** POINT 9 *** Handle the received result data carefully and
            // securely, even though the data came from a partner application.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            Toast.makeText(mContext,
                           String.format("Received \"%s\" from service.", info),
                           Toast.LENGTH_SHORT).show();
        }
    }
}
PkgCertWhitelists.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.pm.PackageManager;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class PkgCertWhitelists {
    private Map<String, String> mWhitelists = new HashMap<String, String>();
        
    public boolean add(String pkgname, String sha256) {
        if (pkgname == null) return false;
        if (sha256 == null) return false;
                
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64)
            return false;  // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0)
            return false;  // found non hex char
                
        mWhitelists.put(pkgname, sha256);
        return true;
    }
        
    public boolean test(Context ctx, String pkgname) {
        // Get the correct hash value which corresponds to pkgname.
        String correctHash = mWhitelists.get(pkgname);
                
        // Compare the actual hash value of pkgname with the correct hash value.
        if (Build.VERSION.SDK_INT >= 28) {
            // ** if API Level >= 28, direct checking is possible
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname,
                                            Utils.hex2Bytes(correctHash),
                                            CERT_INPUT_SHA256);
        } else {
            // else use the facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

4.4.1.4. Creating/Using In-house Services

In-house Services are the Services which are prohibited to be used by applications other than in-house applications. They are used in applications developed internally that want to securely share information and functionality.

Following is an example which uses Messenger bind type Service.

Points (Creating a Service):

  1. Define an in-house signature permission.
  2. Require the in-house signature permission.
  3. Explicitly set exported = “true” without defining the intent filter.
  4. Verify that the in-house signature permission is defined by an in-house application.
  5. Handle the received intent carefully and securely, even though the intent was sent from an in-house application.
  6. Sensitive information can be returned since the requesting application is in-house.
  7. When exporting an APK, sign the APK with the same developer key as the requesting application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.inhouseservice.messenger" >

  <!-- *** POINT 1 *** Define an in-house signature permission -->
  <permission
      android:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION"
      android:protectionLevel="signature" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >

    <!-- Service using Messenger -->
    <!-- *** POINT 2 *** Require the in-house signature permission -->
    <!-- *** POINT 3 *** Explicitly set exported = "true" without defining the intent filter. -->
    <service
        android:name="org.jssec.android.service.inhouseservice.messenger.InhouseMessengerService"
        android:exported="true"
        android:permission="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" />
  </application>

</manifest>
InhouseMessengerService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.inhouseservice.messenger;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Iterator;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.widget.Toast;

public class InhouseMessengerService extends Service{
    // In-house signature permission
    private static final String MY_PERMISSION =
        "org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION";
        
    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of debug.keystore "androiddebugkey"
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of keystore "my company key"
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }
        
        
    // Manage clients(destinations of sending data) in a list
    private ArrayList<Messenger> mClients = new ArrayList<Messenger>();
    
    // Messenger used when service receive data from client
    private final Messenger mMessenger =
            new Messenger(new ServiceSideHandler(mClients));
    
    // Handler which handles message received from client
    private static class ServiceSideHandler extends Handler{

        private ArrayList<Messenger> mClients;

        public ServiceSideHandler(ArrayList<Messenger> clients){
            mClients = clients;
        }

        @Override
        public void handleMessage(Message msg){
            switch(msg.what){
            case CommonValue.MSG_REGISTER_CLIENT:
                // Add messenger received from client
                mClients.add(msg.replyTo);
                break;
            case CommonValue.MSG_UNREGISTER_CLIENT:
                mClients.remove(msg.replyTo);
                break;
            case CommonValue.MSG_SET_VALUE:
                // Send data to client
                sendMessageToClients(mClients);
                break;
            default:
                super.handleMessage(msg);
                break;
            }
        }
    }
    
    /**
     * Send data to client
     */
    private static void sendMessageToClients(ArrayList<Messenger> mClients){
        
        // *** POINT 6 *** Sensitive information can be returned since the
        // requesting application is in-house.
        String sendValue = "Sensitive information (from Service)";
        
        // Send data to the registered client one by one.
        // Use iterator to send all clients even though clients are removed in the
        // loop process.
        Iterator<Messenger> ite = mClients.iterator();
        while(ite.hasNext()){
            try {
                Message sendMsg =
                    Message.obtain(null, CommonValue.MSG_SET_VALUE, null);

                Bundle data = new Bundle();
                data.putString("key", sendValue);
                sendMsg.setData(data);

                Messenger next = ite.next();
                next.send(sendMsg);
                
            } catch (RemoteException e) {
                // If client does not exits, remove it from a list.
                ite.remove();
            }
        }
    }
    
    @Override
    public IBinder onBind(Intent intent) {
                
        // *** POINT 4 *** Verify that the in-house signature permission is
        // defined by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this,"In-house defined signature permission is not defined by in-house application.", Toast.LENGTH_LONG).show();
            return null;
        }

        // *** POINT 5 *** Handle the received intent carefully and securely,
        // even though the intent was sent from an in-house application.
        // Omitted, since this is a sample. Please refer to
        // "3.2 Handling Input Data Carefully and Securely."
        String param = intent.getStringExtra("PARAM");
        Toast.makeText(this,
                       String.format("Received parameter \"%s\".", param),
                       Toast.LENGTH_LONG).show();

        return mMessenger.getBinder();
    }

    @Override
    public void onCreate() {
        Toast.makeText(this, "Service - onCreate()", Toast.LENGTH_SHORT).show();
    }
    
    @Override
    public void onDestroy() {
        Toast.makeText(this, "Service - onDestroy()", Toast.LENGTH_SHORT).show();
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 7 *** When exporting an APK, sign the APK with the same developer key as the requesting application.

_images/image35.png

Fig. 4.4.2 Sign the APK with the same developer key as the requesting application

Next is the sample code of Activity which uses in house only Service.

Points (Using a Service):

  1. Declare to use the in-house signature permission.
  2. Verify that the in-house signature permission is defined by an in-house application.
  3. Verify that the destination application is signed with the in-house certificate.
  4. Sensitive information can be sent since the destination application is in-house.
  5. Use the explicit intent to call an in-house service.
  6. Handle the received result data carefully and securely, even though the data came from an in-house application.
  7. When exporting an APK, sign the APK with the same developer key as the destination application.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.service.inhouseservice.messengeruser" >

  <queries>
    <package android:name="org.jssec.android.service.inhouseservice.messenger" />
  </queries>
  
  <!-- *** POINT 8 *** Declare to use the in-house signature permission.  -->
  <uses-permission
      android:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name="org.jssec.android.service.inhouseservice.messengeruser.InhouseMessengerUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>
InhouseMessengerUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.service.inhouseservice.messengeruser;

import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.view.View;
import android.widget.Toast;

public class InhouseMessengerUserActivity extends Activity {

    private boolean mIsBound;
    private Context mContext;

    // Destination (Requested) service application information
    private static final String TARGET_PACKAGE =
        "org.jssec.android.service.inhouseservice.messenger";
    private static final String TARGET_CLASS =
        "org.jssec.android.service.inhouseservice.messenger.InhouseMessengerService";

    // In-house signature permission
    private static final String MY_PERMISSION =
        "org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of debug.keystore "androiddebugkey"
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of keystore "my company key"
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    // Messenger used when this application receives data from service.
    private Messenger mServiceMessenger = null;

    // Messenger used when this application sends data to service.
    private final Messenger mActivityMessenger =
            new Messenger(new ActivitySideHandler());

    // Handler which handles message received from service
    private class ActivitySideHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case CommonValue.MSG_SET_VALUE:
                Bundle data = msg.getData();
                String info = data.getString("key");
                // *** POINT 13 *** Handle the received result data carefully and
                // securely, even though the data came from an in-house application
                // Omitted, since this is a sample. Please refer to
                // "3.2 Handling Input Data Carefully and Securely."
                Toast.makeText(mContext,
                        String.format("Received \"%s\" from service.", info),
                        Toast.LENGTH_SHORT).show();
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    // Connection used to connect with service. This is necessary when service is
    // implemented with bindService().
    private ServiceConnection mConnection = new ServiceConnection() {

            // This is called when the connection with the service has been
            // established.
            @Override
            public void onServiceConnected(ComponentName className,
                                           IBinder service) {
                mServiceMessenger = new Messenger(service);
                Toast.makeText(mContext,
                               "Connect to service",
                               Toast.LENGTH_SHORT).show();

                try {
                    // Send own messenger to service
                    Message msg =
                        Message.obtain(null, CommonValue.MSG_REGISTER_CLIENT);
                    msg.replyTo = mActivityMessenger;
                    mServiceMessenger.send(msg);
                } catch (RemoteException e) {
                    // Service stopped abnormally
                }
            }

            // This is called when the service stopped abnormally and connection
            // is disconnected.
            @Override
            public void onServiceDisconnected(ComponentName className) {
                mServiceMessenger = null;
                Toast.makeText(mContext,
                               "Disconnected from service",
                               Toast.LENGTH_SHORT).show();
            }
        };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.inhouseservice_activity);

        mContext = this;
    }
    
    // --- StartService control ---
    public void onStartServiceClick(View v) {
        // Start bindService
        doBindService();
    }

    // -- GetInfo control --
    public void onGetInfoClick(View v) {
        getServiceinfo();
    }

    // -- StopService control --
    public void onStopServiceClick(View v) {
        doUnbindService();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        doUnbindService();
    }

    /**
     * Connect to service
     */
    void doBindService() {
        if (!mIsBound){
            // *** POINT 9 *** Verify that the in-house signature permission is
            // defined by an in-house application.
            if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
                Toast.makeText(this, "In-house defined signature permission is not defined by in-house application.", Toast.LENGTH_LONG).show();
                return;
            }

            // *** POINT 10 *** Verify that the destination application is signed
            // with the in-house certificate.
            if (!PkgCert.test(this, TARGET_PACKAGE, myCertHash(this))) {
                Toast.makeText(this, "Destination(Requested) service application is not in-house application.", Toast.LENGTH_LONG).show();
                return;
            }

            Intent intent = new Intent();

            // *** POINT 11 *** Sensitive information can be sent since the
            // destination application is in-house one.
            intent.putExtra("PARAM", "Sensitive information");

            // *** POINT 12 *** Use the explicit intent to call an in-house
            // service.
            intent.setClassName(TARGET_PACKAGE, TARGET_CLASS);

            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
        }
    }

    /**
     * Disconnect service
     */
    void doUnbindService() {
        if (mIsBound) {
            unbindService(mConnection);
            mIsBound = false;
        }
    }

    /**
     * Get information from service
     */
    void getServiceinfo() {
        if (mServiceMessenger != null) {
            try {
                // Request sending information
                Message msg = Message.obtain(null, CommonValue.MSG_SET_VALUE);
                mServiceMessenger.send(msg);
            } catch (RemoteException e) {
                // Service stopped abnormally
            }
        }
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point14 *** When exporting an APK, sign the APK with the same developer key as the destination application.

_images/image35.png

Fig. 4.4.3 Sign the APK with the same developer key as the destination application

4.4.2. Rule Book

Implementing or using service, follow the rules below.

  1. Service that Is Used Only in an application, Must Be Set as Private (Required)
  2. Handle the Received Data Carefully and Securely (Required)
  3. Use the In-house Defined Signature Permission after Verifying If it’s Defined by an In-house Application (Required)
  4. Do Not Determine Whether the Service Provides its Functions, in onCreate (Required)
  5. When Returning a Result Information, Pay Attention the Result Information Leakage from the Destination Application (Required)
  6. Use the Explicit Intent if the Destination Service Is fixed (Required)
  7. Verify the Destination Service If Linking with the Other Company’s Application (Required)
  8. When Providing an Asset Secondarily, the Asset should be protected with the Same Level Protection (Required)
  9. Sensitive Information Should Not Be Sent As Much As Possible (Recommended)

4.4.2.1. Service that Is Used Only in an application, Must Be Set as Private (Required)

Service that is used only in an application (or in same UID) must be set as Private. It avoids the application from receiving Intents from other applications unexpectedly and eventually prevents from damages such as application functions are used or application behavior becomes abnormal.

All you have to do in implementation is set exported attribute false when defining Service in AndroidManifest.xml.

AndroidManifest.xml
    <!-- Private Service derived from Service class -->
    <!-- *** 4.4.1.1 - POINT 1 *** Explicitly set the exported attribute to false. -->
    <service android:name=".PrivateStartService" android:exported="false"/>

In addition, this is a rare case, but do not set Intent Filter when service is used only within the application. The reason is that, due to the characteristics of Intent Filter, public service in other application may be called unexpectedly though you intend to call Private Service within the application.

AndroidManifest.xml(Not recommended)
    <!-- Private Service derived from Service class -->
    <!-- *** 4.4.1.1 - POINT 1 *** Explicitly set the exported attribute to false. -->
    <service android:name=".PrivateStartService" android:exported="false">
        <intent-filter>
            <action android:name=”org.jssec.android.service.OPEN />
        </intent-filter>
    </service>

See “4.4.3.1. Combination of Exported Attribute and Intent-filter Setting (In the Case of Service)”.

4.4.2.2. Handle the Received Data Carefully and Securely (Required)

Same like Activity, In case of Service, when processing a received Intent data, the first thing you should do is input validation. Also in Service user side, it’s necessary to verify the safety of result information from Service. Please refer to “4.1.2.5. Handling the Received Intent Carefully and Securely (Required)” and “4.1.2.9. Handle the Returned Data from a Requested Activity Carefully and Securely (Required).”

In Service, you should also implement calling method and exchanging data by Message carefully.

Please refer to “3.2. Handling Input Data Carefully and Securely

4.4.2.3. Use the In-house Defined Signature Permission after Verifying If it’s Defined by an In-house Application (Required)

Make sure to protect your in-house Services by defining in-house signature permission when creating the Service. Since defining a permission in the AndroidManifest.xml file or declaring a permission request does not provide adequate security, please be sure to refer to “5.2.1.2. How to Communicate Between In-house Applications with In-house-defined Signature Permission.”

4.4.2.4. Do Not Determine Whether the Service Provides its Functions, in onCreate (Required)

Security checks such as Intent parameter verification or in-house-defined Signature Permission verification should not be included in onCreate, because when receiving new request during Service is running, process of onCreate is not executed. So, when implementing Service which is started by startService, judgment should be executed by onStartCommand (In case of using IntentService, judgment should be executed by onHandleIntent.) It’s also same in the case when implementing Service which is started by bindService, judgment should be executed by onBind.

4.4.2.5. When Returning a Result Information, Pay Attention the Result Information Leakage from the Destination Application (Required)

Depends on types of Service, the reliability of result information destination application (callback receiver side/ Message destination) are different. Need to consider seriously about the information leakage considering the possibility that the destination may be Malware.

See, Activity “4.1.2.7. When Returning a Result, Pay Attention to the Possibility of Information Leakage of that Result from the Destination Application (Required)”, for details.

4.4.2.6. Use the Explicit Intent if the Destination Service Is fixed (Required)

When using a Service by implicit Intents, in case the definition of Intent Filter is same, Intent is sent to the Service which was installed earlier. If Malware with the same Intent Filter defined intentionally was installed earlier, Intent is sent to Malware and information leakage occurs. On the other hand, when using a Service by explicit Intents, only the intended Service will receive the Intent so this is much safer.

There are some other points which should be considered, please refer to “4.1.2.8. Use the explicit Intents if the destination Activity is predetermined. (Required).”

4.4.2.7. Verify the Destination Service If Linking with the Other Company’s Application (Required)

Be sure to sure a whitelist when linking with another company’s application. You can do this by saving a copy of the company’s certificate hash inside your application and checking it with the certificate hash of the destination application. This will prevent a malicious application from being able to spoof Intents. Please refer to sample code section “4.4.1.3. Creating/Using Partner Services” for the concrete implementation method.

4.4.2.8. When Providing an Asset Secondarily, the Asset should be protected with the Same Level Protection (Required)

When an information or function asset, which is protected by permission, is provided to another application secondhand, you need to make sure that it has the same required permissions needed to access the asset. In the Android OS permission security model, only an application that has been granted proper permissions can directly access a protected asset. However, there is a loophole because an application with permissions to an asset can act as a proxy and allow access to an unprivileged application. Substantially this is the same as re-delegating permission so it is referred to as the “Permission Re-delegation” problem. Please refer to “5.2.3.4. Permission Re-delegation Problem.”

4.4.2.9. Sensitive Information Should Not Be Sent As Much As Possible (Recommended)

You should not send sensitive information to untrusted parties.

You need to consider the risk of information leakage when exchanging sensitive information with a Service. You must assume that all data in Intents sent to a Public Service can be obtained by a malicious third party. In addition, there is a variety of risks of information leakage when sending Intents to Partner or In-house Services as well depending on the implementation.

Not sending sensitive data in the first place is the only perfect solution to prevent information leakage therefore you should limit the amount of sensitive information being sent as much as possible. When it is necessary to send sensitive information, the best practice is to only send to a trusted Service and to make sure the information cannot be leaked through LogCat.

4.4.3. Advanced Topics

4.4.3.1. Combination of Exported Attribute and Intent-filter Setting (In the Case of Service)

We have explained how to implement the four types of Services in this guidebook: Private Services, Public Services, Partner Services, and In-house Services. The various combinations of permitted settings for each type of exported attribute defined in the AndroidManifest.xml file and the intent-filter elements are defined in the table below. Please verify the compatibility of the exported attribute and intent-filter element with the Service you are trying to create.

Table 4.4.3 Combination of Exported Attribute and intent-filter Setting
  Value of exported attribute
true false Not specified
Intent Filter defined (Do not Use) (Do not Use) (Do not Use)
Intent Filter Not Defined Public, Partner, In-house Private (Do not Use)

If the exported attribute is not unspecified in a Service, the question of whether or not the Service is public is determined by whether or not intent filters are defined [15]; however, in this guidebook it is forbidden to set a Service’s exported attribute to “unspecified”. In general, as mentioned previously, it is best to avoid implementations that rely on the default behavior of any given API; moreover, in cases where explicit methods exist for configuring important security-related settings such as the exported attribute, it is always a good idea to make use of those methods.

[15]If any intent filters are defined then the Service is public; otherwise it is private. For more information, see https://developer.android.com/guide/topics/manifest/service-element#exported

In “Table 4.4.3 Combination of Exported Attribute and intent-filter Setting”, all “Intent Filter defined” are set to “(Do not Use)”. This is because when a Service is started using an implicit Intent, it is not possible to know which Service responds to the Intent, and a malicious Service may respond.

And the reason why “a defined intent filter and an exported attribute of false” should not be used is that there is a loophole in Android’s behavior, and because of how Intent filters work, other application’s Services can be called unexpectedly.

Concretely, Android behaves as per below, so it’s necessary to consider carefully when application designing.

  • When multiple Services define the same content of intent-filter, the definition of Service within application installed earlier is prioritized.
  • In case explicit Intent is used, prioritized Service is automatically selected and called by OS.

The system that unexpected call is occurred due to Android’s behavior is described in the three figures below. Fig. 4.4.4 is an example of normal behavior that Private Service (application A) can be called by implicit Intent only from the same application. Because only application A defines Intent-filter (action=”X” in the Figure), it behaves normally. This is the normal behavior.

_images/image45.png

Fig. 4.4.4 An Example of Normal Behavior

Fig. 4.4.5 and Fig. 4.4.6 below show a scenario in which the same Intent filter (action=”X”) is defined in Application B as well as Application A.

Fig. 4.4.5 shows the scenario that applications are installed in the order, application A -> application B. In this case, when application C sends implicit Intent, calling Private Service (A-1) fails. On the other hand, since application A can successfully call Private Service within the application by implicit Intent as expected, there won’t be any problems in terms of security (counter-measure for Malware).

_images/image46.png

Fig. 4.4.5 Applications are installed in the order, application A -> application B

Fig. 4.4.6 shows the scenario that applications are installed in the order, applicationB -> applicationA. There is a problem here, in terms of security. It shows an example that applicationA tries to call Private Service within the application by sending implicit Intent, but actually Public Activity (B-1) in application B which was installed earlier, is called. Due to this loophole, it is possible that sensitive information can be sent from applicationA to applicationB. If applicationB is Malware, it will lead the leakage of sensitive information.

_images/image47.png

Fig. 4.4.6 Applications are installed in the order, applicationB -> applicationA

As shown above, using Intent filters to send implicit Intents to Private Service may result in unexpected behavior so it is best to avoid this setting.

4.4.3.2. How to Implement Service

Because methods for Service implementation are various and should be selected with consideration of security type which is categorized by sample code, each characteristics are briefly explained. It’s divided roughly into the case using startService and the case using bindService. And it’s also possible to create Service which can be used in both startService and bindService. Following items should be investigated to determine the implementation method of Service.

  • Whether to disclose Service to other applications or not (Disclosure of Service)
  • Whether to exchange data during running or not (Mutual sending/receiving data)
  • Whether to control Service or not (Launch or complete)
  • Whether to execute as another process (communication between processes)
  • Whether to execute multiple processes in parallel (Parallel process)

Table 4.4.4 shows category of implementation methods and feasibility of each item.

“NG” stands for impossible case or case that another frame work which is different from the provided function is required.

Table 4.4.4 Category of implementation methods for Service
Category Disclosure of Service Mutual sending/receiving data Control Service (Boot/Exit) Communication between processes Parallel process
startService type OK NG OK OK NG
IntentService type OK NG NG OK NG
local bind type NG OK OK NG NG
Messenger bind type OK OK OK OK NG
AIDL bind type OK OK OK OK OK
startService type

This is the most basic Service. This inherits Service class, and executes processes by onStartCommand.

In user side, specify Service by Intent, and call by startService. Because data such as results cannot be returned to source of Intent directly, it should be achieved in combination with another method such as Broadcast. Please refer to “4.4.1.1. Creating/Using Private Services” for the concrete example.

Checking in terms of security should be done by onStartCommand, but it cannot be used for partner only Service since the package name of the source cannot be obtained.

IntentService type

IntentService is the class which was created by inheriting Service. Calling method is same as startService type. Following are characteristics compared with standard service (startService type.)

  • Processing Intent is done by onHandleIntent (onStartCommand is not used.)
  • It’s executed by another thread.
  • Process is to be queued.

Call is immediately returned because process is executed by another thread, and process towards Intents is sequentially executed by Queuing system. Each Intent is not processed in parallel, but it is also selectable depending on the product’s requirement, as an option to simplify implementation. Since data such as results cannot be returned to source of Intent, it should be achieved in combination with another method such as Broadcast. Please refer to “4.4.1.2. Creating/Using Public Services” for the concrete example of implementation.

Checking in terms of security should be done by onHandleIntent, but it cannot be used for partner only Service since the package name of the source cannot be obtained.

local bind type

This is a method to implement local Service which works only within the process same as an application. Define the class which was derived from Binder class, and prepare to provide the feature (method) which was implemented in Service to caller side.

From user side, specify Service by Intent and call Service by using bindService. This is the most simple implementation method among all methods of binding Service, but it has limited usages since it cannot be launched by another process and also Service cannot be disclosed. See project “Service PrivateServiceLocalBind” which is included in Sample code, for the concrete implementation example.

From the security point of view, only private Service can be implemented.

Messenger bind type

This is the method to achieve the linking with Service by using Messenger system.

Since Messenger can be given as a Message destination from Service user side, the mutual data exchanging can be achieved comparatively easily. In addition, since processes are to be queued, it has a characteristic that behaves “thread-safely”. Parallel process for each process is not possible, but it is also selectable as an option to simplify the implementation depending on the product’s requirement. Regarding user side, specify Service by Intent, and call Service by using bindService. See “4.4.1.4. Creating/Using In-house Services” for the concrete implementation example.

Security check in onBind or by Message Handler is necessary, however, it cannot be used for partner only Service since package name of source cannot be obtained.

AIDL bind type

This is a method to achieve linking with Service by using AIDL system. Define interface by AIDL, and provide features that Service has as a method. In addition, call back can be also achieved by implementing interface defined by AIDL in user side, Multi-thread calling is possible, but it’s necessary to implement explicitly in Service side for exclusive process.

User side can call Service, by specifying Intent and using bindService. Please refer to “4.4.1.3. Creating/Using Partner Services” for the concrete implementation example.

Security must be checked in onBind for In-house only Service and by each method of interface defined by AIDL for partner only Service.

This can be used for all security types of Service which are described in this Guidebook.

4.5. Using SQLite

Herein after, some cautions in terms of security when creating/operating database by using SQLite. Main points are appropriate setting of access right to database file, and counter-measures for SQL injection. Database which permits reading/writing database file from outside directly (sharing among multiple applications) is not supposed here, but suppose the usage in backend of Content Provider and in an application itself. In addition, it is recommended to adopt counter-measures mentioned below in case of handling not so much sensitive information, though handling a certain level of sensitive information is supposed here.

4.5.1. Sample Code

4.5.1.1. Creating/Operating Database

When handling database in Android application, appropriate arrangements of database files and access right setting (Setting for denying other application’s access) can be achieved by using SQLiteOpenHelper [16]. Here is an example of easy application that creates database when it’s launched, and executes searching/adding/changing/deleting data through UI. Sample code is what counter-measure for SQL injection is done, to avoid from incorrect SQL being executed against the input from outside.

[16]As regarding file storing, the absolute file path can be specified as the 2nd parameter (name) of SQLiteOpenHelper constructor. Therefore, need attention that the stored files can be read and written by the other applications if the SD Card path is specified.
_images/image48.png

Fig. 4.5.1 Using Database in Android Application

Points:

  1. SQLiteOpenHelper should be used for database creation.
  2. Use place holder.
  3. Validate the input value according the application requirements.
SampleDbOpenHelper.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.sqlite;

import android.content.Context;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.widget.Toast;

public class SampleDbOpenHelper extends SQLiteOpenHelper {
    private SQLiteDatabase mSampleDb;  //Database to store the data to be handled

    public static SampleDbOpenHelper newHelper(Context context)
    {
        //*** POINT 1 *** SQLiteOpenHelper should be used for database creation.
        return new SampleDbOpenHelper(context);
    }

    public SQLiteDatabase getDb() {
        return mSampleDb;
    }

    //Open DB by Writable mode
    public void openDatabaseWithHelper() {
        try {
            if (mSampleDb != null && mSampleDb.isOpen()) {
                if (!mSampleDb.isReadOnly())//  Already opened by writable mode
                    return;
                mSampleDb.close();
            }
            mSampleDb  = getWritableDatabase(); //It's opened here.
        } catch (SQLException e) {
            //In case fail to construct database, output to log
            Log.e(mContext.getClass().toString(),
                    mContext.getString(R.string.DATABASE_OPEN_ERROR_MESSAGE));
            Toast.makeText(mContext,
                           R.string.DATABASE_OPEN_ERROR_MESSAGE,
                           Toast.LENGTH_LONG).show();
        }
    }

    //Open DB by ReadOnly mode.
    public void openDatabaseReadOnly() {
        try {
            if (mSampleDb != null && mSampleDb.isOpen()) {
                if (mSampleDb.isReadOnly())// Already opened by ReadOnly.
                    return;
                mSampleDb.close();
            }
            SQLiteDatabase.openDatabase(mContext.getDatabasePath(CommonData.DBFILE_NAME).getPath(),null, SQLiteDatabase.OPEN_READONLY);
        } catch (SQLException e) {
            //In case failed to construct database, output to log
            Log.e(mContext.getClass().toString(),
                  mContext.getString(R.string.DATABASE_OPEN_ERROR_MESSAGE));
            Toast.makeText(mContext,
                           R.string.DATABASE_OPEN_ERROR_MESSAGE,
                           Toast.LENGTH_LONG).show();
        }
    }

    //Database Close
    public void closeDatabase() {
        try {
            if (mSampleDb != null && mSampleDb.isOpen()) {
                mSampleDb.close();
            }
        } catch (SQLException e) {
            //In case failed to construct database, output to log
            Log.e(mContext.getClass().toString(),
                  mContext.getString(R.string.DATABASE_CLOSE_ERROR_MESSAGE));
            Toast.makeText(mContext,
                           R.string.DATABASE_CLOSE_ERROR_MESSAGE,
                           Toast.LENGTH_LONG).show();
        }
    }

    //Remember Context
    private Context mContext;

    //Table creation command
    private static final String CREATE_TABLE_COMMANDS
        = "CREATE TABLE " + CommonData.TABLE_NAME + " ("
        + "_id INTEGER PRIMARY KEY AUTOINCREMENT, "
        + "idno INTEGER UNIQUE, "
        + "name VARCHAR(" + CommonData.TEXT_DATA_LENGTH_MAX + ") NOT NULL, "
        + "info VARCHAR(" + CommonData.TEXT_DATA_LENGTH_MAX + ")"
        + ");";

    public SampleDbOpenHelper(Context context) {
        super(context, CommonData.DBFILE_NAME, null, CommonData.DB_VERSION);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        try {
            db.execSQL(CREATE_TABLE_COMMANDS);  //Execute DB construction command 
        } catch (SQLException e) {
            //In case failed to construct database, output to log
            Log.e(this.getClass().toString(),
                    mContext.getString(R.string.DATABASE_CREATE_ERROR_MESSAGE));
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
        // It's to be executed when database version up. Write processes like data
        // transition.
    }
}
DataSearchTask.java(SQLite Database Project)
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.sqlite.task;

import org.jssec.android.sqlite.CommonData;
import org.jssec.android.sqlite.DataValidator;
import org.jssec.android.sqlite.MainActivity;
import org.jssec.android.sqlite.R;

import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.util.Log;


//Data search task
public class DataSearchTask extends AsyncTask<String, Void, Cursor> {
    private MainActivity    mActivity;
    private SQLiteDatabase  mSampleDB;

    public DataSearchTask(SQLiteDatabase db, MainActivity activity) {
        mSampleDB = db;
        mActivity = activity;
    }

    @Override
    protected Cursor doInBackground(String... params) {
        String  idno = params[0];
        String  name = params[1];
        String  info = params[2];
        String  cols[]  =   {"_id", "idno","name","info"};

        Cursor cur;

        //*** POINT 3 *** Validate the input value according the application
        // requirements.
        if (!DataValidator.validateData(idno, name, info))
            {
                return null;
            }

        //When all parameters are null, execute all search
        if ((idno == null || idno.length() == 0) &&
            (name == null || name.length() == 0) &&
            (info == null || info.length() == 0) ) {
            try {
                cur = mSampleDB.query(CommonData.TABLE_NAME,
                        cols, null, null, null, null, null);
            } catch (SQLException e) {
                Log.e(DataSearchTask.class.toString(),
                        mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE));
                return null;
            }
            return cur;
        }

        //When No is specified, execute searching by No  
        if (idno != null && idno.length() > 0) {
            String selectionArgs[] = {idno};

            try {
                //*** POINT 2 *** Use place holder.
                cur = mSampleDB.query(CommonData.TABLE_NAME, cols,
                        "idno = ?", selectionArgs, null, null, null);
            } catch (SQLException e) {
                Log.e(DataSearchTask.class.toString(),
                        mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE));
                return null;
            }
            return cur;
        }

        //When Name is specified, execute perfect match search by Name
        if (name != null && name.length() > 0) {
            String selectionArgs[] = {name};
            try {
                //*** POINT 2 *** Use place holder.
                cur = mSampleDB.query(CommonData.TABLE_NAME, cols,
                        "name = ?", selectionArgs, null, null, null);
            } catch (SQLException e) {
                Log.e(DataSearchTask.class.toString(),
                        mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE));
                return null;
            }
            return cur;
        }

        //Other than above, execute partly match searching with the condition
        // of info.
        //Escape @ in info which was received as input.
        String argString = info.replaceAll("@", "@@");
        //Escape % in info which was received as input.
        argString = argString.replaceAll("%", "@%");
        //Escape _ in info which was received as input.
        argString = argString.replaceAll("_", "@_");
        String selectionArgs[] = {argString};

        try {
            //*** POINT 2 *** Use place holder.
            cur = mSampleDB.query(CommonData.TABLE_NAME, cols,
                    "info LIKE '%' || ? || '%' ESCAPE '@'",
                    selectionArgs, null, null, null);
        } catch (SQLException e) {
            Log.e(DataSearchTask.class.toString(),
                    mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE));
            return null;
        }
        return cur;
    }

    @Override
    protected void onPostExecute(Cursor resultCur) {
        mActivity.updateCursor(resultCur);
    }
}
DataValidator.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.sqlite;

public class DataValidator {
    //Validate the Input value
    //validate numeric characters
    public static boolean validateNo(String idno) {
        //null and blank are OK
        if (idno == null || idno.length() == 0) {
            return true;
        }

        //Validate that it's numeric character.
        try {
            if (!idno.matches("[1-9][0-9]*")) {
                //Error if it's not numeric value 
                return false;
            }
        } catch (NullPointerException e) {
            //Detected an error
            return false;
        }

        return true;
    }

    // Validate the length of a character string
    public static boolean validateLength(String str, int max_length) {
        //null and blank are OK
        if (str == null || str.length() == 0) {
            return true;
        }

        //Validate the length of a character string is less than MAX
        try {
            if (str.length() > max_length) {
                //When it's longer than MAX, error
                return false;
            }
        } catch (NullPointerException e) {
            //Bug
            return false;
        }

        return true;
    }

    // Validate the Input value
    public static boolean validateData(String idno, String name, String info) {
        if (!validateNo(idno)) {
            return false;
        }
        if (!validateLength(name, CommonData.TEXT_DATA_LENGTH_MAX)) {
            return false;
        }else if(!validateLength(info, CommonData.TEXT_DATA_LENGTH_MAX)) {
            return false;
        }    
        return true;
    }
}

4.5.2. Rule Book

Using SQLite, follow the rules below accordingly.

  1. Set DB File Location and Access Right Correctly (Required)
  2. Use Content Provider for Access Control When Sharing DB Data with Other Application (Required)
  3. Place Holder Must Be Used in the Case Handling Variable Parameter during DB Operation. (Required)

4.5.2.1. Set DB File Location and Access Right Correctly (Required)

Considering the protection of DB file data, DB file location and access right setting is the very important elements that need to be considered together.

For example, even if file access right is set correctly, a DB file can be accessed from anybody in case that it is arranged in a location which access right cannot be set, e.g. SD card. And in case that it’s arranged in application directory, if the access right is not correctly set, it will eventually allow the unexpected access. Following are some points to be met regarding the correct allocation and access right setting, and the methods to realize them.

About location and access right setting, considering in terms of protecting DB file (data), it’s necessary to execute 2 points as per below.

  1. Location

Locate in file path that can be obtained by Context#getDatabasePath(String name), or in some cases, directory that can be obtained by Context#getFilesDir [17].

  1. Access right

Set to MODE_PRIVATE ( = it can be accessed only by the application which creates file) mode.

[17]Both methods provide the path under (package) directory which is able to be read and written only by the specified application.

By executing following 2 points, DB file which cannot be accessed by other applications can be created. Here are some methods to execute them.

1. Use SQLiteOpenHelper

2. Use Context#openOrCreateDatabase

When creating DB file, SQLiteDatabase#openOrCreateDatabase can be used. However, when using this method, DB files which can be read out from other applications are created, in some Android smartphone devices. So it is recommended to avoid this method, and using other methods. Each characteristics for the above 2 methods are as per below.

Using SQLiteOpenHelper

When using SQLiteOpenHelper, developers don’t need to be worried about many things. Create a class derived from SQLiteOpenHelper, and specify DB name (which is used for file name) [18] to constructer’s parameter, then DB file which meets above security requirements, are to be created automatically.

Refer to specific usage method for “4.5.1.1. Creating/Operating Database” for how to use.

[18](Undocumented in Android reference) Since the full file path can be specified as the database name in SQLiteOpenHelper implementation, need attention that specifying the place (path) which does not have access control feature (e.g. SD cards) unintentionally.
Using Context#openOrCreateDatabase

When creating DB by using Context#openOrCreateDatabase method, file access right should be specified by option, in this case specify MODE_PRIVATE explicitly.

Regarding file arrangement, specifying DB name (which is to be used to file name) can be done as same as SQLiteOpenHelper, a file is to be created automatically, in the file path which meets the above mentioned security requirements. However, full path can be also specified, so it’s necessary to pay attention that when specifying SD card, even though specifying MODE_PRIVATE, other applications can also access.

Example to execute access permission setting to DB explicitly: MainActivity.java

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //Construct database
        try {
            //Create DB by setting MODE_PRIVATE
            db = Context.openOrCreateDatabase("Sample.db", MODE_PRIVATE, null);
        } catch (SQLException e) {
            //In case failed to construct DB, log output
            Log.e(this.getClass().toString(),
                  getString(R.string.DATABASE_OPEN_ERROR_MESSAGE));
            return;
        }
        //Omit other initial process
    }

There are three possible settings for access privileges: MODE_PRIVATE, MODE_WORLD_READABLE, and MODE_WORLD_WRITEABLE. These constants can be specified together by “OR” operator. However, all settings other than MODE_PRIVATE are deprecated in API Level 17 and later versions, and will result in a security exception in API Level 24 and later versions. Even for apps intended for API Level 15 and earlier, it is generally best not to use these flags [19].

  • MODE_PRIVATE Only creator application can read and write
  • MODE_WORLD_READABLE Creator application can read and write, Others can only read in
  • MODE_WORLD_WRITEABLE Creator application can read and write, Others can only write in
[19]For more information as to MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE and points of caution regarding their use, see Section “4.6.3.2. Access Permission Setting for the Directory”.

4.5.2.2. Use Content Provider for Access Control When Sharing DB Data with Other Application (Required)

The method to share DB data with other application is that create DB file as WORLD_READABLE, WORLD_WRITEABLE, to other applications to access directly. However, this method cannot limit applications which access to DB or operations to DB, so data can be read-in or written by unexpected party (application). As a result, it can be considered that some problems may occur in confidentiality or consistency of data, or it may be an attack target of Malware.

As mentioned above, when sharing DB data with other applications in Android, it’s strongly recommended to use Content Provider. By using Content Provider, there are some merits, not only the merits from the security point of view which is the access control on DB can be achieved, but also merits from the designing point of view which is DB scheme structure can be hidden into Content Provider.

4.5.2.3. Place Holder Must Be Used in the Case Handling Variable Parameter during DB Operation. (Required)

In the sense that preventing from SQL injection, when incorporating the arbitrary input value to SQL statement, placeholder should be used. There are 2 methods as per below to execute SQL using placeholder.

  1. Get SQLiteStatement by using SQLiteDatabase#compileStatement(), and after that place parameter to placeholder by using SQLiteStatement#bindString() or bindLong() etc.
  2. When calling execSQL(), insert(), update(), delete(), query(), rawQuery() and replace() in SQLiteDatabase class, use SQL statement which has placeholder.

In addition, when executing SELECT command, by using SQLiteDatabase#compileStatement(), there is a limitation that “only the top 1 element can be obtained as a result of SELECT command”, so usages are limited.

In either method, the data content which is given to placeholder is better to be checked in advance according the application requirements. Following is the further explanation for each method.

When Using SQLiteDatabase#compileStatement():

Data is given to placeholder in the following steps.

  1. Get the SQL statement which includes placeholder by using SQLiteDatabase#compileStatement(), as SQLiteStatement.
  2. Set the created as SQLiteStatement objects to placeholder by using the method like bindLong() and bindString().
  3. Execute SQL by method like execute() of ExecSQLiteStatement object.

Use case of placeholder: DataInsertTask.java (an extra)

//Adding data task
public class DataInsertTask extends AsyncTask<String, Void, Void> {
    private MainActivity    mActivity;
    private SQLiteDatabase  mSampleDB;

    public DataInsertTask(SQLiteDatabase db, MainActivity activity) {
        mSampleDB = db;
        mActivity = activity;
    }

    @Override
    protected Void doInBackground(String... params) {
        String  idno = params[0];
        String  name = params[1];
        String  info = params[2];

        // *** POINT 3 *** Validate the input value according the application
        // requirements.
        if (!DataValidator.validateData(idno, name, info))
        {
            return null;
        }
        // Adding data task
        // *** POINT 2 *** Use place holder
        String commandString = "INSERT INTO " + CommonData.TABLE_NAME + 
                               " (idno, name, info) VALUES (?, ?, ?)";
        SQLiteStatement sqlStmt = mSampleDB.compileStatement(commandString);
        sqlStmt.bindString(1, idno);
        sqlStmt.bindString(2, name);
        sqlStmt.bindString(3, info);
        try {
            sqlStmt.executeInsert();
        } catch (SQLException e) {
            Log.e(DataInsertTask.class.toString(),
                  mActivity.getString(R.string.UPDATING_ERROR_MESSAGE));
        } finally {
            sqlStmt.close();
        }
        return null;
    }
    ... Abbreviation ...
}

This is a type that SQL statement to be executed as object is created in advance, and parameters are allocated to it. The process to execute is fixed, so there’s no room for SQL injection to occur. In addition, there is a merit that process efficiency is enhanced by reutilizing SQLiteStatement object.

In the Case Using Method for Each Process which SQLiteDatabase provides:

There are 2 types of DB operation methods that SQLiteDatabase provides. One is what SQL statement is used, and another is what SQL statement is not used. Methods that SQL statement is used are SQLiteDatabase#execSQL()/rawQuery() and it’s executed in the following steps.

  1. Prepare SQL statement which includes placeholder.
  2. Create data to allocate to placeholder.
  3. Send SQL statement and data as parameter, and execute a method for process.

On the other hand, SQLiteDatabase#insert()/update()/delete()/query()/replace() is the method that SQL statement is not used. When using them, data should be sent as per the following steps.

  1. In case there’s data to insert/update to DB, register to ContentValues.
  2. Send ContentValues as parameter, and execute a method for each process (In the following example, SQLiteDatabase#insert())

Use case of metod for each process (SQLiteDatabase#insert())

    private SQLiteDatabase  mSampleDB;
    private void addUserData(String idno, String name, String info) {

        // Validity check of the value(Type, range), escape process
        if (!validateInsertData(idno, name, info)) {
            // If failed to pass the validation, log output
            Log.e(this.getClass().toString(),
                  getString(R.string.VALIDATION_ERROR_MESSAGE));
            return;
        }

        // Prepare data to insert
        ContentValues insertValues = new ContentValues();
        insertValues.put("idno", idno);
        insertValues.put("name", name);
        insertValues.put("info", info);

        // Execute Insert
        try {
            mSampleDb.insert("SampleTable", null, insertValues);
        } catch (SQLException e) {
            Log.e(this.getClass().toString(),
                  getString(R.string.DB_INSERT_ERROR_MESSAGE));
            return;
        }
    }

In this example, SQL command is not directly written, for instead, a method for inserting which SQLiteDatabase provides, is used. SQL command is not directly used, so there’s no room for SQL injection in this method, too.

4.5.3. Advanced Topics

4.5.3.1. When Using Wild Card in LIKE Predicate of SQL Statement, Escape Process Should Be Implemented

When using character string which includes wild card (%, _) of LIKE predicate, as input value of place holder, it will work as a wild card unless it is processed properly, so it’s necessary to implement escape process in advance according the necessity. It is the case which escape process is necessary that wild card should be used as a single character (”%” or “_”).

The actual escape process is executed by using ESCAPE clause as per below sample code.

Example of ESCAPE process in case of using LIKE

// Data search task
public class DataSearchTask extends AsyncTask<String, Void, Cursor> {
    private MainActivity        mActivity;
    private SQLiteDatabase      mSampleDB;
    private ProgressDialog      mProgressDialog;

    public DataSearchTask(SQLiteDatabase db, MainActivity activity) {
        mSampleDB = db;
        mActivity = activity;
    }

    @Override
    protected Cursor doInBackground(String... params) {
        String  idno = params[0];
        String  name = params[1];
        String  info = params[2];
        String  cols[]  =   {"_id", "idno","name","info"};

        Cursor cur;

        ... Abbreviation ...

        // Execute like search(partly match) with the condition of info
        // Point: Escape process should be performed on characters
        // which is applied to wild card
        // Escape @ in info which was received as input
        String argString = info.replaceAll("@", "@@");
        // Escape % in info which was received as input
        argString = argString.replaceAll("%", "@%");
        // Escape _ in info which was received as input
        argString = argString.replaceAll("_", "@_");
        String selectionArgs[] = {argString};

        try {
            // Point: Use place holder
            cur = mSampleDB.query("SampleTable", cols, 
                                  "info LIKE '%' || ? || '%' ESCAPE '@'",
                                  selectionArgs, null, null, null);
        } catch (SQLException e) {
            Toast.makeText(mActivity,
                R.string.SERCHING_ERROR_MESSAGE, Toast.LENGTH_LONG).show();
            return null;
        }
        return cur;
    }

    @Override
    protected void onPostExecute(Cursor resultCur) {
        mProgressDialog.dismiss();
        mActivity.updateCursor(resultCur);
    }
}

4.5.3.2. Use External Input to SQL Command in which Place Holder Cannot Be Used

When executing SQL statement which process targets are DB objects like table creation/deletion etc., placeholder cannot be used for the value of table name. Basically, DB should not be designed using arbitrary character string which was input from outside in case that placeholder cannot be used for the value.

When placeholder cannot be used due to the restriction of specifications or features, whether the Input value is dangerous or not, should be verified before execution, and it’s necessary to implement necessary processes.

Basically,

  1. When using as character string parameter, escape or quote process for character should be made.
  2. When using as numeric value parameter, verify that characters other than numeric value are not included.
  3. When using as identifier or command, verify whether characters which cannot be used are not included, along with 1.

should be executed.

4.5.3.3. Take a Countermeasure that Database Is Not Overwritten Unexpectedly

In case getting instance of DB by SQLiteOpenHelper#getReadableDatabase, getWriteableDatabase, DB is to be opened in readable/WRITEABLE state by using either method [20]. In addition, it’s same to Context#openOrCreateDatabase, SQLiteDatabase#openOrCreateDatabase, etc.

[20]getReableDatabase() returns the same object which can be got by getWritableDatabase. This spec is, in case writable object cannot be generated due to disc full etc., it will return Read- only object. (getWritableDatabase() will be execution error under the situation like disc full etc.)

It means that contents of DB may be overwritten unexpectedly by application operation or by defects in implementation. Basically, it can be supported by the application’s spec and range of implementation, but when implementing the function which requires only read in function like application’s searching function etc., opening database by read-only, it may lead to simplify designing or inspection and furthermore, lead to enhance application quality, so it’s recommended depends on the situation.

Specifically, open database by specifying OPEN_READONLY to SQLiteDatabase#openDatabase.

Open database by read-only.

    ... Abbreviation ...

    // Open DB(DB should be created in advance)
    SQLiteDatabase db
        = SQLiteDatabase.openDatabase(SQLiteDatabase.getDatabasePath("Sample.db"),
                                      null, OPEN_READONLY);

4.5.3.4. Verify the Validity of Input/Output Data of DB, According to Application’s Requirement

SQLite is the database which is tolerant types, and it can store character type data into columns which is declared as Integer in DB. Regarding data in database, all data including numeric value type is stored in DB as character data of plain text. So searching of character string type, can be executed to Integer type column. (LIKE ‘%123%’ etc.) In addition, the limitation for the value in SQLite (validity verification) is untrustful since data which is longer than limitation can be input in some case, e.g. VARCHAR(100).

So, applications which use SQLite, need to be very careful about this characteristics of DB, and it is necessary take actions according to application requirements, not to store unexpected data to DB or not to get unexpected data. Countermeasures are as per below 2 points.

  1. When storing data in database, verify that type and length are matched.
  2. When getting the value from database, verify whether data is beyond the supposed type and length, or not.

Following is an example of the code which verifies that the Input value is more than 1.

Verify that the Input value is more than 1 (Extract from MainActivity.java)

public class MainActivity extends Activity {

    ... Abbreviation ...

    // Process for adding
    private void addUserData(String idno, String name, String info) {
        // Check for No
        if (!validateNo(idno, CommonData.REQUEST_NEW)) {
            return;
        }

        // Inserting data process
        DataInsertTask task = new DataInsertTask(mSampleDb, this);
        task.execute(idno, name, info);
    }

    ... Abbreviation ...

    private boolean validateNo(String idno, int request) {
        if (idno == null || idno.length() == 0) {
            if (request == CommonData.REQUEST_SEARCH) {
                // When search process, unspecified is considered as OK.
                return true;
            } else {
                // Other than search process, null and blank are error.
                Toast.makeText(this,
                    R.string.IDNO_EMPTY_MESSAGE, Toast.LENGTH_LONG).show();
                return false;
            }
        }

        // Verify that it's numeric character
        try {
            // Value which is more than 1
            if (!idno.matches("[1-9][0-9]*")) {
                // In case of not numeric character, error
                Toast.makeText(this, R.string.IDNO_NOT_NUMERIC_MESSAGE,
                    Toast.LENGTH_LONG).show();
                return false;
            }
        } catch (NullPointerException e) {
            // It never happen in this case
            return false;
        }
        return true;
    }

    ... Abbreviation ...
}

4.5.3.5. Consideration - the Data Stored into Database

In SQLite implementation, when storing data to file is as per below.

  • All data including numeric value type are stored into DB file as character data of plain text.
  • When executing data deletion to DB, data itself is not deleted form DB file. (Only deletion mark is added.)
  • When updating data, data before updating has not been deleted, and still remains there in DB file.

So, the information which “must have” been deleted may still remain in DB file. Even in this case, take counter-measures according this Guidebook, and when Android security function is enabled, data/file may not be directly accessed by the third party including other applications. However, considering the case that files are picked out by passing through Android’s protection system like root privilege is taken, in case the data which gives huge influence on business is stored, data protection which doesn’t depend on Android protection system, should be considered.

As above reasons, the important data which is necessary to be protected even when device’s root privilege is taken, should not be stored in DB of SQLite, as it is. In case need to store the important data, it’s necessary to implement counter-measures, or encrypt overall DB.

When encryption is necessary, there are so many issues that are beyond the range of this Guidebook, like handling the key which is used for encryption or code obfuscation, so as of now it’s recommended to consult the specialist when developing an application which handles data that has huge business impact.

Please refer to “4.5.3.6. [Reference] Encrypt SQLite Database (SQLCipher for Android)” library which encrypts database is introduced here.

4.5.3.6. [Reference] Encrypt SQLite Database (SQLCipher for Android)

Developed by Zetetic LLC, SQLCipher provides transparent 256-bit AES encryption of SQLite databases. It is an SQLite extension library implemented in C language, and it uses OpenSSL for encryption. It also provides APIs for Obj-C, Java, Python, and other languages. In addition to the commercial version, an open source version (called “community edition”) is also available, and it can be used for commercial purposes with a BSD license. It supports a wide range of platforms including Windows, Linux, macOS, and more, and in the mobile space, besides Android, it is also widely used in Nokia / QT and Apple’s iOS.

Among these versions, SQLCipher for Android was packaged specifically for Android use [21]. Although content can be created by compiling from the available source code, a library is also distributed in AAR format (android-database-sqlcipher-xxxx.aar), and this may convenient for simple usage [22]. Some standard SQLite APIs can be changed to match SQLCipher to enable developers to use databases encrypted with the same coding as usual. This section provides a brief introduction of how to use libraries in AAR format.

Reference: https://www.zetetic.net/sqlcipher/

[21]https://github.com/sqlcipher/android-database-sqlcipher
[22]In these explanations, xxxx is the version number of the library, and the latest version at the time of this writing was 3.5.9. The explanations below assume use of this version.
How to Use

The following procedure is used in Android Studio to enable use of SQLCipher.

  1. Place android-database-sqlcipher-3.5.9.aar in the libs directory of the application. (https://www.zetetic.net/sqlcipher/open-source/)

  2. Specify the dependency in app/gradle.

    dependencies {
        :
        implementation 'net.zetetic:android-database-sqlcipher:3.5.9@aar'
        :
    }
    
  3. Instead of the normal android.database.sqlite.*, import net.sqlcipher.database.*. (The android.database.Cursor can be used without any changes.)

  4. Before using the database, load and initialize the library, and specify the password when opening the database.

The code shown below is used to execute the initialization process for using the database. Before an activity uses the database, it is assumed that SQLCipherInitializer.Initialize() is called. First, SQLiteDatabase.loadLibs(this) is called, and then the required library is loaded and initialized. Also, when a database is opened using SQLiteDatabase.openOrCreateDatabase(), the password is passed. The database is encrypted using an encryption key generated based on the password provided here. The key point here is that a database created in plain text cannot be converted into an encrypted database later, and the password must be specified when the database is created.

package android.jssec.org.samplesqlcipher;

import android.content.Context;
// instead of the normal android.database.sqlite*, import net.sqlcipher.database*
import net.sqlcipher.database.SQLiteDatabase;
import java.io.File;

public class SQLCipherInitializer {
    static SQLiteDatabase Initialize(Context ctx, String dbName, String password) {
        // before using DB, load neccessary libraries and initialize
        SQLiteDatabase.loadLibs(ctx);
        // create databe file uder the package local directory
        File databaseFile = ctx.getDatabasePath(dbName);
        // password must be specified when the databse is created
        return SQLiteDatabase.openOrCreateDatabase(databaseFile, password, null);
    }
}

The above shows an example using SQLiteDatabase.openOrCreateDatabase(), but the SQLiteOpenHelper#getWritableDatabase() and SQLiteOpenHelper#getReadableDatabase() APIs have been modified so that the password can be passed as an argument. In either case, if null is specified for the password, a normal SQLite database is created without encrypting the database.

Another key point is that Context#openOrCreateDatabase() cannot be used. As a result, it is not possible to force setting of protection mode for the database files or force creation of a database in the local directory of the package. Consequently, when a database is created using SQLiteDatabase.openOrCreateDatabase(), at the minimum, as shown in the example above, it is recommended that a database be created in the database directory of the package itself using getDatabasePath(). On the other hand, there are no modifications to the constructor API of SQLiteOpenHelper, and if this is used, a database is created in the local directory of the package in the same way as android.database.sqlite.SQLiteOpenHelper.

4.6. Handling Files

According to Android security designing idea, files are used only for making information persistence and temporary save (cache), and it should be private in principle. Exchanging information between applications should not be direct access to files, but it should be exchanged by inter-application linkage system, like Content Provider or Service. By using this, inter-application access control can be achieved.

Since enough access control cannot be performed on external memory device like SD card etc., so it should be limited to use only when it’s necessary by all means in terms of function, like when handling huge size files or transferring information to another location (PC etc.). Basically, files that include sensitive information should not be saved in external memory device. In case sensitive information needs to be saved in a file of external device at any rate, counter-measures like encryption are necessary, but it’s not referred here.

4.6.1. Sample Code

As mentioned above, files should be private in principle. However, sometimes files should be read out/written by other applications directly for some reasons. File types which are categorized from the security point of view and comparison are shown in Table 4.6.1. These are categorized into 4 types of files based on the file storage location or access permission to other application. Sample code for each file category is shown below and explanation for each of them are also added there.

Table 4.6.1 File category and comparison from security point of view
File category Access permission to other application Storage location Overview
Private file NA In application directory
  • Can read and write only in an application.
  • Sensitive information can be handled.
  • File should be this type in principle.
Read out public file Read out In application directory
  • Other applications and users can read.
  • Information that can be disclosed to outside of application is handled.
  • The MODE_WORLD_READABLE variable used to create a public file is deprecated from API level 17, and will trigger a security exception from API level 24.
Read write public file Read out/Write in In application directory
  • Other applications and users can read and write.
  • It should not be used from both security and application designing points of view.
External memory device (Read write public) Read out/Write in External memory device like SD card
  • No access control.
  • Other applications and users can always read/write/delete files.
  • Usage should be minimum requirement.
  • Comparatively huge size of files cn be handled.
  • Use filtered view in API Level 29 or later.
External memory device (API Level 29 or later) Read out/Write in External memory device like SD card
  • The filtered view for external storage can be used to save the app file to the app-specific directory.

  • To access files that other apps have created, both of the following conditions must be true.

    • The app has been granted the READ_EXTERNAL_STORAGE permission
    • The files reside in one of the following media collections: MediaStore.Images, MediaStore.Video, or MediaStore.Audio
  • In order to access any other file (including files in a downloads directory), the app must use the Storage Access Framework.

  • Comparatively huge size of files cn be handled.

4.6.1.1. Using Private Files

This is the case to use files that can be read/written only in the same application, and it is a very safe way to use files. In principle, whether the information stored in the file is public or not, keep files private as much as possible, and when exchanging the necessary information with other applications, it should be done using another Android system (Content Provider, Service.)

Points:

  1. Files must be created in application directory.
  2. The access privilege of file must be set private mode in order not to be used by other applications.
  3. Sensitive information can be stored.
  4. Regarding the information to be stored in files, handle file data carefully and securely.
PrivateFileActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.privatefile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PrivateFileActivity extends Activity {

    private TextView mFileView;

    private static final String FILE_NAME = "private_file.dat";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.file);

        mFileView = (TextView) findViewById(R.id.file_view);
    }

    /**
     * Create file process
     * 
     * @param view
     */
    public void onCreateFileClick(View view) {
        FileOutputStream fos = null;
        try {
            // *** POINT 1 *** Files must be created in application directory.
            // *** POINT 2 *** The access privilege of file must be set private
            // mode in order not to be used by other applications.
            fos = openFileOutput(FILE_NAME, MODE_PRIVATE);

            // *** POINT 3 *** Sensitive information can be stored.
            // *** POINT 4 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            fos.write(new String("Not sensotive information (File Activity)\n")
                                .getBytes());
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PrivateFileActivity",
                               "failed to read file");
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    android.util.Log.e("PrivateFileActivity",
                                       "failed to close file");
                }
            }
        }

        finish();
    }

    /**
     * Read file process
     * 
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            fis = openFileInput(FILE_NAME);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PrivateFileActivity",
                               "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("PrivateFileActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Delete file process
     * 
     * @param view
     */
    public void onDeleteFileClick(View view) {

        File file = new File(this.getFilesDir() + "/" + FILE_NAME);
        file.delete();

        mFileView.setText(R.string.file_view);
    }
}
PrivateUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.privatefile;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PrivateUserActivity extends Activity {

    private TextView mFileView;

    private static final String FILE_NAME = "private_file.dat";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user);
        mFileView = (TextView) findViewById(R.id.file_view);
    }

    private void callFileActivity() {
        Intent intent = new Intent();
        intent.setClass(this, PrivateFileActivity.class);

        startActivity(intent);
    }

    /**
     *  Call file Activity process
     * 
     * @param view
     */
    public void onCallFileActivityClick(View view) {
        callFileActivity();
    }

    /**
     *  Read file process
     * 
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            fis = openFileInput(FILE_NAME);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            // *** POINT 4 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PrivateUserActivity",
                               "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("PrivateUserActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Rewrite file process
     * 
     * @param view
     */
    public void onWriteFileClick(View view) {
        FileOutputStream fos = null;
        try {
            // *** POINT 1 *** Files must be created in application directory.
            // *** POINT 2 *** The access privilege of file must be set private
            // mode in order not to be used by other applications.
            fos = openFileOutput(FILE_NAME, MODE_APPEND);

            // *** POINT 3 *** Sensitive information can be stored.
            // *** POINT 4 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            fos.write(new String("Sensitive information (User Activity)\n").getBytes());
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PrivateUserActivity",
                               "failed to read file");
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    android.util.Log.e("PrivateUserActivity",
                                       "failed to close file");
                }
            }
        }

        callFileActivity();
    }
}

4.6.1.2. Using Public Read Only Files

This is the case to use files to disclose the contents to unspecified large number of applications. If you implement by following the below points, it’s also comparatively safe file usage method. Note that using the MODE_WORLD_READABLE variable to create a public file is deprecated in API Level 17 and later versions, and will trigger a security exception in API Level 24 and later versions, therefore, the following sample code does not work ;thus file-sharing methods using Content Provider are preferable.

Points:

  1. Files must be created in application directory.
  2. The access privilege of file must be set to read only to other applications.
  3. Sensitive information must not be stored.
  4. Regarding the information to be stored in files, handle file data carefully and securely.
PublicFileActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.publicfile.readonly;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PublicFileActivity extends Activity {

    private TextView mFileView;

    private static final String FILE_NAME = "public_file.dat";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.file);

        mFileView = (TextView) findViewById(R.id.file_view);
    }

    /**
     * Create file process
     * 
     * @param view
     */
    public void onCreateFileClick(View view) {
        FileOutputStream fos = null;
        try {
            // *** POINT 1 *** Files must be created in application directory.
            // *** POINT 2 *** The access privilege of file must be set to read
            // only to other applications.
            // (MODE_WORLD_READABLE is deprecated API Level 17,
            // don't use this mode as much as possible and exchange data by using
            // ContentProvider().)
            fos = openFileOutput(FILE_NAME, MODE_WORLD_READABLE);

            // *** POINT 3 *** Sensitive information must not be stored.
            // *** POINT 4 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            fos.write(new String("Not sensitive information (Public File Activity)\n").getBytes());
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PublicFileActivity",
                               "failed to read file");
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    android.util.Log.e("PublicFileActivity",
                                       "failed to close file");
                }
            }
        }

        finish();
    }

    /**
     * Read file process
     * 
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            fis = openFileInput(FILE_NAME);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("PublicFileActivity",
                               "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("PublicFileActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Delete file process
     * 
     * @param view
     */
    public void onDeleteFileClick(View view) {

        File file = new File(this.getFilesDir() + "/" + FILE_NAME);
        file.delete();

        mFileView.setText(R.string.file_view);
    }
}
PublicUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.publicuser.readonly;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class PublicUserActivity extends Activity {

    private TextView mFileView;

    private static final String TARGET_PACKAGE =
            "org.jssec.android.file.publicfile.readonly";
    private static final String TARGET_CLASS =
            "org.jssec.android.file.publicfile.readonly.PublicFileActivity";

    private static final String FILE_NAME = "public_file.dat";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user);
        mFileView = (TextView) findViewById(R.id.file_view);
    }

    private void callFileActivity() {
        Intent intent = new Intent();
        intent.setClassName(TARGET_PACKAGE, TARGET_CLASS);

        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            mFileView.setText("(File Activity does not exist)");
        }
    }

    /**
     * Call file Activity process
     * 
     * @param view
     */
    public void onCallFileActivityClick(View view) {
        callFileActivity();
    }

    /**
     * Read file process
     * 
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            File file = new File(getFilesPath(FILE_NAME));
            fis = new FileInputStream(file);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            // *** POINT 4 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            android.util.Log.e("PublicUserActivity", "no file");
        } catch (IOException e) {
            android.util.Log.e("PublicUserActivity", "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("PublicUserActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Rewrite file process
     * 
     * @param view
     */
    public void onWriteFileClick(View view) {
        FileOutputStream fos = null;
        boolean exception = false;
        try {
            File file = new File(getFilesPath(FILE_NAME));
            // Fail to write in. FileNotFoundException occurs. 
            fos = new FileOutputStream(file, true);

            fos.write(new String("Not sensitive information (Public User Activity)\n").getBytes());
        } catch (IOException e) {
            mFileView.setText(e.getMessage());
            exception = true;
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    exception = true;
                }
            }
        }

        if (!exception)
            callFileActivity();
    }

    private String getFilesPath(String filename) {
        String path = "";

        try {
            Context ctx = createPackageContext(TARGET_PACKAGE,
                                               Context.CONTEXT_RESTRICTED);
            File file = new File(ctx.getFilesDir(), filename);
            path = file.getPath();
        } catch (NameNotFoundException e) {
            android.util.Log.e("PublicUserActivity", "no file");
        }
        return path;
    }
}

4.6.1.3. Using Public Read/Write Files

This is the usage of the file which permits read-write access to unspecified large number of application.

Unspecified large number of application can read and write, means that needless to say. Malware can also read and write, so the credibility and safety of data will be never guaranteed. In addition, even in case of not malicious intention, data format in file or timing to write in cannot be controlled. So this type of file is almost not practical in terms of functionality.

As above, it’s impossible to use read-write files safely from both security and application designing points of view, so using read-write files should be avoided.

Point:

  1. Must not create files that be allowed to read/write access from other applications.

4.6.1.4. Using Eternal Memory (Read Write Public) Files

This is the case when storing files in an external memory like SD card. It’s supposed to be used when storing comparatively huge information (placing file which was downloaded from Web), or when bring out the information to outside (backup etc.).

“External memory file (Read Write public)” has the equal characteristics with “Read Write public file” to unspecified large number of applications. In addition, it has the equal characteristics with “Read Write public file” to applications which declares to use android.permission.WRITE_EXTERNAL_STORAGE Permission. So, the usage of “External memory file (Read Write public) file” should be minimized as less as possible.

A Backup file is most probably created in an external memory device as Android application’s customary practice. However, as mentioned as above, files in an external memory have the risk that is tampered/deleted by other applications including malware. Hence, in applications which output backup, some contrivances to minimize risks in terms of application spec or designing like displaying a caution “Copy Backup files to the safety location like PC etc., a.s.a.p.”, are necessary.

Because the filtered view for external storage (see “4.6.3.6. About specifications related to access to external storage in Android 10 (API Level 29)”) is used as the default in Android 10 (API level 29), the following sample code (user side) does not run. However, the manifest attribute requestLegacyExternalStorage can be set to temporarily opt out of the scoped storage function. This is used only for temporary applications before the app is fully compatible or before app testing, and its use in the release version and other versions is not allowed. In the next major platform release, it is expected that scoped storage will be required in all apps regardless of the target SDK level.

Points:

  1. Sensitive information must not be stored.
  2. Files must be stored in the unique directory per application.
  3. Regarding the information to be stored in files, handle file data carefully and securely.
  4. Writing file by the requesting application should be prohibited as the specification.

Sample code for create

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.file.externalfile" >

  <!-- declare android.permission.WRITE_EXTERNAL_STORAGE permission to write to the external strage --> 
  <!-- In Android 4.4 (API Level 19) and later, the application, which read/write only files in its specific
       directories on external storage media, need not to require the permission and it should declare
       the maxSdkVersion -->
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                   android:maxSdkVersion="18"/>

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name=".ExternalFileActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
ExternalFileActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.externalfile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class ExternalFileActivity extends Activity {

    private TextView mFileView;

    private static final String TARGET_TYPE = "external";

    private static final String FILE_NAME = "external_file.dat";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.file);

        mFileView = (TextView) findViewById(R.id.file_view);
    }

    /**
     * Create file process
     * 
     * @param view
     */
    public void onCreateFileClick(View view) {
        FileOutputStream fos = null;
        try {
            // *** POINT 1 *** Sensitive information must not be stored.
            // *** POINT 2 *** Files must be stored in the unique directory per
            // application.
            File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME);
            fos = new FileOutputStream(file, false);

            // *** POINT 3 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            fos.write(new String("Non-Sensitive Information(ExternalFileActivity)\n").getBytes());
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("ExternalFileActivity",
                               "failed to read file");
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    android.util.Log.e("ExternalFileActivity",
                                       "failed to close file");
                }
            }
        }

        finish();
    }

    /**
     * Read file process
     * 
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME);
            fis = new FileInputStream(file);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            // *** POINT 3 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("ExternalFileActivity",
                               "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("ExternalFileActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Delete file process
     * 
     * @param view
     */
    public void onDeleteFileClick(View view) {

        File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME);
        file.delete();

        mFileView.setText(R.string.file_view);
    }
}

Sample code for use

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.file.externaluser" >

  <queries>
    <package android:name="org.jssec.android.file.externalfile" />
  </queries>

  <!-- In Android 4.0.3 (API Level 14) and later, the permission for reading external storages
       has been defined and the application should decalre that it requires the permission. 
       In fact in Android 4.4 (API Level 19) and later, that must be declared to read other directories 
       than the package specific directories. -->
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >

    <activity
        android:name=".ExternalUserActivity"
        android:label="@string/app_name"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
ExternalUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.externaluser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class ExternalUserActivity extends Activity {

    private TextView mFileView;

    private static final String TARGET_PACKAGE =
            "org.jssec.android.file.externalfile";
    private static final String TARGET_CLASS =
            "org.jssec.android.file.externalfile.ExternalFileActivity";
    private static final String TARGET_TYPE = "external";

    private static final String FILE_NAME = "external_file.dat";
    private final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1000;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user);
        mFileView = (TextView) findViewById(R.id.file_view);
        // Android 6.0 (API level 23) or later requires dangerous permission
        // (in this case READ_EXTERNAL_STORAGE permission)
        // must be granted at runtime by user.
        // (Refer to “5.2.3.6. Modification to the Permission model Specifications
        // in Android versions 6.0 and later”)
        if (Build.VERSION.SDK_INT >= 23) {
            if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[],
                                           int[] grantResults) {
        if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
            if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                finish();
            }
        }
    }

    private void callFileActivity() {
        Intent intent = new Intent();
        intent.setClassName(TARGET_PACKAGE, TARGET_CLASS);

        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            mFileView.setText("(File Activity does not exist)");
        }
    }

    /**
     * Call file Activity process
     *
     * @param view
     */
    public void onCallFileActivityClick(View view) {
        callFileActivity();
    }

    /**
     * Read file process
     *
     * @param view
     */
    public void onReadFileClick(View view) {
        FileInputStream fis = null;
        try {
            File file = new File(getFilesPath(FILE_NAME));
            fis = new FileInputStream(file);

            byte[] data = new byte[(int) fis.getChannel().size()];

            fis.read(data);

            // *** POINT 3 *** Regarding the information to be stored in files,
            // handle file data carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            String str = new String(data);

            mFileView.setText(str);
        } catch (FileNotFoundException e) {
            mFileView.setText(R.string.file_view);
        } catch (IOException e) {
            android.util.Log.e("ExternalUserActivity",
                               "failed to read file");
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    android.util.Log.e("ExternalUserActivity",
                                       "failed to close file");
                }
            }
        }
    }

    /**
     * Rewrite file process
     *
     * @param view
     */
    public void onWriteFileClick(View view) {

        // *** POINT 4 *** Writing file by the requesting application should be
        // prohibited as the specification.
        // Application should be designed supposing malicious application may
        // overwrite or delete file.

        final AlertDialog.Builder alertDialogBuilder =
                new AlertDialog.Builder(this);
        alertDialogBuilder.setTitle("POINT 4");
        alertDialogBuilder.setMessage("Do not write in calling appllication.");
        alertDialogBuilder.setPositiveButton("OK",
                new DialogInterface.OnClickListener() {

                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callFileActivity();
                }
        });

        alertDialogBuilder.create().show();

    }

    private String getFilesPath(String filename) {
        String path = "";

        try {
            Context ctx = createPackageContext(TARGET_PACKAGE,
                                               Context.CONTEXT_IGNORE_SECURITY);
            File file = new File(ctx.getExternalFilesDir(TARGET_TYPE), filename);
            path = file.getPath();
        } catch (NameNotFoundException e) {
            android.util.Log.e("ExternalUserActivity", "no file");
        }
        return path;
    }
}

4.6.2. Rule Book

Handling files follow the rules below.

  1. File Must Be Created as a Private File in Principle (Required)
  2. Must Not Create Files that Be Allowed to Read/Write Access from Other Applications (Required)
  3. Using Files Stored in External Device (e.g. SD Card) Should Be Requisite Minimum (Required)
  4. Application Should Be Designed Considering the Scope of File (Required)

4.6.2.1. File Must Be Created as a Private File in Principle (Required)

As mentioned in “4.6. Handling Files” and “4.6.1.1. Using Private Files,” regardless of the contents of the information to be stored, files should be set private, in principle. From Android security designing point of view, exchanging information and its access control should be done in Android system like Content Provider and Service, etc., and in case there’s a reason that is impossible, it should be considered to be substituted by file access permission as alternative method.

Please refer to sample code of each file type and following rule items.

4.6.2.2. Must Not Create Files that Be Allowed to Read/Write Access from Other Applications (Required)

As mentioned in “4.6.1.3. Using Public Read/Write Files,” when permitting other applications to read/write files, information stored in files cannot be controlled. So, sharing information by using read/write public files should not be considered from both security and function/designing points of view.

4.6.2.3. Using Files Stored in External Device (e.g. SD Card) Should Be Requisite Minimum (Required)

As mentioned in “4.6.1.4. Using Eternal Memory (Read Write Public) Files,” storing files in external memory device like SD card, leads to holding the potential problems from security and functional points of view. On the other hand, SD card can handle files which have longer scope, compared with application directory, and this is the only one storage that can be always used to bring out the data to outside of application. So, there may be many cases that cannot help using it, depends on application’s spec.

When storing files in external memory device, considering unspecified large number of applications and users can read/write/delete files, so it’s necessary that application is designed considering the points as per below as well as the points mentioned in sample code.

  • Sensitive information should not be saved in a file of external memory device, in principle.
  • In case sensitive information is saved in a file of external memory device, it should be encrypted.
  • In case saving in a file of external memory device information that will be trouble if it’s tampered by other application or users, it should be saved with electrical signature.
  • When reading in files in external memory device, use data after verifying the safety of data to read in.
  • Application should be designed supposing that files in external memory device can be always deleted.

Please refer to “4.6.2.4. Application Should Be Designed Considering the Scope of File (Required).”

4.6.2.4. Application Should Be Designed Considering the Scope of File (Required)

Data saved in application directory is deleted by the following user operations. It’s consistent with the application’s scope, and it’s distinctive that it’s shorter than the scope of application.

  • Uninstalling application.
  • Delete data and cache of each application. (”Setting” > “Apps” > “select target application”)

Files that were saved in external memory device like SD card, it’s distinctive that the scope of the file is longer than the scope of the application. In addition, the following situations are also necessary to be considered.

  • File deletion by user
  • Pick off/replace/unmount SD card
  • File deletion by Malware

As mentioned above, since scope of files are different depends on the file saving location, not only from the viewpoint to protect sensitive information, but also form view point to achieve the right behavior as application, it’s necessary to select the file save location.

4.6.3. Advanced Topics

4.6.3.1. File Sharing Through File Descriptor

There is a method to share files through file descriptor, not letting other applications access to public files. This method can be used in Content Provider and in Service. Opponent application can read/write files through file descriptors which are got by opening private files in Content Provider or in Service.

Comparison between the file sharing method of direct access by other applications and the file sharing method via file descriptor, is as per below Table 4.6.2. Variation of access permission and range of applications that are permitted to access, can be considered as merits. Especially, from security point of view, this is a great merit that, applicaions that are permitted to accesss can be controlled in detail.

Table 4.6.2 Comparison of inter-application file sharing method
File sharing method Variation or access permission setting Range of applications that are permitted to access
File sharing that permits other applications to access files directry
  • Read in
  • Write in
  • Read in + Write in
Give all application access permissions equally
File sharing through file descriptor
  • Read in
  • Write in
  • Only add
  • Read in + Write in
  • Read in + Only add
Can control whether to give access permission or not, to application which try to access indivisually and temporarily, to Content provider or Service

This is common in both of above file sharing methods, when giving write permission for files to other applications, integrity of file contents are difficult to be guaranteed. When several applications write in in parallel, there’s a risk that data structure of file contents are destroyed, and application doesn’t work normally. So, in sharing files with other applications, giving only read only permission is preferable.

Herein below an implementation example of file sharing by Content Provider and its sample code, are published.

Point

  1. The source application is In house application, so sensitive information can be saved.
  2. Even if it’s a result from In house only Content Provider application, verify the safety of the result data.
InhouseProvider.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.inhouseprovider;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

public class InhouseProvider extends ContentProvider {

    private static final String FILENAME = "sensitive.txt";

    // In-house signature permission
    private static final String MY_PERMISSION =
            "org.jssec.android.file.inhouseprovider.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;

    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of  debug.keystore "androiddebugkey"
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of  keystore "my company key"
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    @Override
    public boolean onCreate() {
        File dir = getContext().getFilesDir();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(new File(dir, FILENAME));
            // *** POINT 1 *** The source application is In house application,
            // so sensitive information can be saved.
            fos.write(new String("Sensitive information").getBytes());

        } catch (IOException e) {
            android.util.Log.e("InhouseProvider", "failed to read file");
        } finally {
            try {
                fos.close();
            } catch (IOException e) {
                android.util.Log.e("InhouseProvider", "failed to close file");
            }
        }

        return true;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode)
        throws FileNotFoundException {

        // Verify that in-house-defined signature permission is defined by
        // in-house application.
        if (!SigPerm
            .test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
            throw new SecurityException("In-house-defined signature permission is not defined by in-house application.");
        }

        File dir = getContext().getFilesDir();
        File file = new File(dir, FILENAME);

        // Always return read-only, since this is sample
        int modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
        return ParcelFileDescriptor.open(file, modeBits);
    }

    @Override
    public String getType(Uri uri) {
        return "";
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
}
InhouseUserActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.file.inhouseprovideruser;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.view.View;
import android.widget.TextView;

public class InhouseUserActivity extends Activity {

    // Content Provider information of destination (requested provider)
    private static final String AUTHORITY =
            "org.jssec.android.file.inhouseprovider";

    // In-house signature permission
    private static final String MY_PERMISSION =
            "org.jssec.android.file.inhouseprovider.MY_PERMISSION";

    // In-house certificate hash value
    private static String sMyCertHash = null;

    private static String myCertHash(Context context) {

        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Certificate hash value of debug.keystore "androiddebugkey"
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Certificate hash value of keystore "my company key"
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    // Get package name of destination (requested) content provider.
    private static String providerPkgname(Context context, String authority) {
        String pkgname = null;
        PackageManager pm = context.getPackageManager();
        ProviderInfo pi = pm.resolveContentProvider(authority, 0);
        if (pi != null)
            pkgname = pi.packageName;
        return pkgname;
    }

    public void onReadFileClick(View view) {

        logLine("[ReadFile]");

        // Verify that in-house-defined signature permission is defined by
        // in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            logLine("  In-house-defined signature permission is not defined by in-house application.");
            return;
        }

        // Verify that the certificate of destination (requested) content provider
        // application is in-house certificate.
        String pkgname = providerPkgname(this, AUTHORITY);
        if (!PkgCert.test(this, pkgname, myCertHash(this))) {
            logLine("  Destination (Requested) Content Provider is not in-house application.");
            return;
        }

        // Only the information which can be disclosed to in-house only content
        // provider application, can be included in a request.
        ParcelFileDescriptor pfd = null;
        try {
            pfd = getContentResolver()
                    .openFileDescriptor(Uri.parse("content://" + AUTHORITY), "r");
        } catch (FileNotFoundException e) {
            android.util.Log.e("InhouseUserActivity", "no file");
        }

        if (pfd != null) {
            FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());

            if (fis != null) {
                try {
                    byte[] buf = new byte[(int) fis.getChannel().size()];
                    fis.read(buf);
                    // *** POINT 2 *** Handle received result data carefully and
                    // securely, even though the data came from in-house
                    // applications.
                    // Omitted, since this is a sample. Please refer to
                    // "3.2 Handling Input Data Carefully and Securely."
                    logLine(new String(buf));
                } catch (IOException e) {
                    android.util.Log.e("InhouseUserActivity",
                                       "failed to read file");
                } finally {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        android.util.Log.e("ExternalFileActivity",
                                           "failed to close file");
                    }
                }
            }
            try {
                pfd.close();
            } catch (IOException e) {
                android.util.Log.e("ExternalFileActivity",
                                   "failed to close file descriptor");
            }

        } else {
            logLine("  null file descriptor");
        }
    }

    private TextView mLogView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView) findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}

4.6.3.2. Access Permission Setting for the Directory

Herein above, security considerations are explained, focusing on files. It’s also necessary to consider the security for directory which is a file container. Herein below, security considerations of access permission setting for directory are explained.

In Android, there are some methods to get/create subdirectory in application directory. The major ones are as per below Table 4.6.3.

Table 4.6.3 Methods to get/create subdirectory in application directory
  Specify access permission to other applications Deletion by User
Context#getFilesDir() Impossible (Only execution permission) “Setting” > “Apps” > select target application > “Clear data”
Context#”getCacheDir() Impossible (Only execution permission) “Setting” > “Apps” > select target application > “Clear cache” (It can be deleted by “Clear data,” too.)
Context#getDir(String name, int mode) modes MODE_PRIVATE, MODE_WORLD_READABLE or MODE_WORLD_WRITEABLE can be specified as a MODE “Setting” > “Apps” > select target application > “Clear data”

Here especially what needs to pay attention is access permission setting by Context#getDir(). As explained in file creation, basically directory also should be set private from the security designing point of view. When sharing information depends on access permission setting, there may be an unexpected side effect, so other methods should be taken as information sharing.

MODE_WORLD_READABLE

This is a flag to give all applications read-only permission to directory. So all applications can get file list and individual file attribute information in the directory. Because secret files may not be placed in these directories, in general this flag must not be used. [23]

[23](1, 2) MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE are deprecated in API Level17 and later versions, and in API Level 24 and later versions their use will trigger a security exception.
MODE_WORLD_WRITEABLE

This flag gives other applications write permission to directory. All applications can create/move [24]/rename/delete files in the directory. These operations has no relation with access permission setting (Read/Write/Execute) of file itself, so it’s necessary to pay attention that operations can be done only with write permission to directory. This flag allows other apps to delete or replace files arbitrarily, so in general it must not be used. [23]

[24]Files cannot be moved over mount point (e.g. from internal storage to external storage). Therefore, moving the protected files from internal storage to external storage cannot be happened.

Regarding Table 4.6.3 “Deletion by User”, refer to “4.6.2.4. Application Should Be Designed Considering the Scope of File (Required).”

4.6.3.3. Access Permission Setting for Shared Preference and Database File

Shared Preference and database also consist of files. Regarding access permission setting what are explained for files are applied here. Therefore, both Shared Preference and database, should be created as private files same like files, and sharing contents should be achieved by the Android’s inter-application linkage system.

Herein below, the usage example of Shared Preference is shown. Shared Preference is crated as private file by MODE_PRIVATE.

Example of setting access restriction to Shared Preference file.

import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

... Abbreviation ...

        // Get Shared Preference.
        // (If there's no Shared Preference, it's to be created.)
        // Point: Basically, specify MODE_PRIVATE mode.
        SharedPreferences preference = getSharedPreferences(
            PREFERENCE_FILE_NAME, MODE_PRIVATE);

        // Example of writing preference which value is charcter string.
        Editor editor = preference.edit();
        // key:"prep_key", value:"prep_value"
        editor.putString("prep_key", "prep_value");
        editor.commit();

Please refer to “4.5. Using SQLite” for database.

4.6.3.4. Specification Change regarding External Storage Access in Android 4.4 (API Level 19) and later

The specification regarding External Storage Access has been changed to the followings since Android 4.4 (API Level 19).

(1) In the case that the application needs read/write to its specific directories on external storage media, the WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE permissions need not to be declared with <uses-permission>. (Changed)

(2) In the case that the application needs read files on other directories than its specific directories on external storage media, the READ_EXTERNAL_STORAGE permission needs to be declared with <uses-permission>. (Changed)

(3) In the case that the application needs to write files on other directories than its specific directories on the primary external storage media, the WRITE_EXTERNAL_STORAGE permission needs to be declared with <uses-permission>.

(4) The application cannot write files on other directories than its specific directories on the secondary external storage media.

In that specification, whether the permission requisitions are needed is determined according to the version of Android OS. So in the case that the application supports the versions including Android 4.3 and 4.4, it could lead to a pleasant situation that the application requires the unnecessary permission of users. Therefore, applications just corresponding to the paragraph (1) is recommended to use the maxSdkVersion attribute of <uses-permission> like the below.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.file.externalfile" >

  <!-- declare android.permission.WRITE_EXTERNAL_STORAGE permission to write to the external strage --> 
  <!-- In Android 4.4 (API Level 19) and later, the application, which read/write only files in its specific
       directories on external storage media, need not to require the permission and it should declare
       the maxSdkVersion -->
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                   android:maxSdkVersion="18"/>

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name=".ExternalFileActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

4.6.3.5. Revised specifications in Android 7.0 (API Level 24) for accessing specific directories on external storage media

On devices running Android 7.0 (API Level 24) or later, a new API known as Scoped Directory Access API has been introduced. “Scoped Directory Access” allows the application to access to specific directories on external storage media without permission.

Within Scoped Directory Access, a directory defined in the Environment class is passed as a parameter to the StorageVolume#createAccessIntent method to create an Intent. By sending this Intent via startActivityForResult, you can enable a situation in which a dialog box requesting access permission appears on the terminal screen, and—if the user grants permission—the specified directories on each storage volume become accessible.

Table 4.6.4 Directories that may be accessed via Scoped Directory Access
DIRECTORY_MUSIC Standard location for general music files
DIRECTORY_PODCASTS Standard directory for podcasts
DIRECTORY_RINGTONES Standard directory for ringtones
DIRECTORY_ALARMS Standard directory for alarms
DIRECTORY_NOTIFICATIONS Standard directory for notifications
DIRECTORY_PICTURES Standard directory for pictures
DIRECTORY_MOVIES Standard directory for movies
DIRECTORY_DOWNLOADS Standard directory for user-downloaded files
DIRECTORY_DCIM Standard directory for image/video files produced by cameras
DIRECTORY_DOCUMENTS Standard directory for user-created documents

If the location to be accessed by an app lies within one of the above directories, and if the app is running on an Android 7.0 or later device, the use of Scoped Directory Access is recommended for the following reasons. For apps that must continue to support pre-Android 7.0 devices, see the sample code in the AndroidManifest listed in Section “4.6.3.4. Specification Change regarding External Storage Access in Android 4.4 (API Level 19) and later”.

  • When a Permission is granted to access external storage, the app is able to access directories other than its intended destination.
  • Using Storage Access Framework to require users to choose accessible directories results in a cumbersome procedure in which the user must configure a selector on each access. Also, when access to the root directory of an external storage is granted, the entirety of that storage becomes accessible.

Because the StorageVolume#createAccessIntent method is deprecated as of Android 10 (API level 29), Intent#ACTION_OPEN_DOCUMENT_TREE is used instead for generating Intent [25].

[25]https://developer.android.com/reference/android/os/storage/StorageVolume

4.6.3.7. Application of Scoped Storage in Android 11 (API Level 30)

Scoped storage is applied for all apps targeted for Android 11 (API Level 30) and later [29].

[29]https://developer.android.com/preview/privacy/storage?hl=en#scoped-storage

The setting of the manifest attribute requestLegacyExternalStorage to opt out scoped storage prepared on Android 10 will be ignored. App exclusive directories will become private and cannot be accessed from any external app.

Managing device storage

The following intents are prepared for file managers and backup apps that manage device storage.

Table 4.6.6 Intents to Manage Storage
ACTION_MANAGE_STORAGE Confirm free space on the storage
ACTION_CLEAR_APP_CACHE Delete the cache after acquiring consent from the user if there is not enough space for saving

Also, in terms of privacy protection, the following user requests by intent are no longer available.

Table 4.6.7 Changed Intents
ACTION_OPEN_DOCUMENT_TREE Request made to users for Android/data/ directory and all subdirectories as well as Android/obb/ directory and all subdirectories
ACTION_OPEN_DOCUMENT Request made to users for Android/data/ directory and all subdirectories as well as Android/obb/ directory and all subdirectories
MANAGE_EXTERNAL_STORAGE

A request must be made to users to acquire MANAGE_EXTERNAL_STORAGE permission if all files such as anti-virus apps must be accessed. The procedure to make requests is as follows.

  • Declare the MANAGE_EXTERNAL_STORAGE permission in the manifest
  • Execute the ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent

Executing the ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent transfers the user to the system setting page. The MANAGE_EXTERNAL_STORAGE permission is granted when the user enables the [Allow access to manage all files] option here.

The following access is allowed when the MANAGE_EXTERNAL_STORAGE permission is granted.

  • Read access permission and write access permission for all files in the shared storage
  • Access permission to contents of the MediaStore.Files table
  • Write access permission to all internal storage directories with some exceptions

However, the following internal storage directories cannot be accessed even if the MANAGE_EXTERNAL_STORAGE permission is granted.

  • /Android/data
  • /sdcard/Android (This does not apply to /sdcard/Android/media since it is a shared storage)

Apps requesting MANAGE_EXTERNAL_STORAGE permission with Android 11 as the target cannot be uploaded to Google Play until early 2021. This upload restriction is a policy due to the effects of COVID-19 and it is recommended that the target SDK should not be updated to Android 11 (API Level 30) for apps that are thought to require MANAGE_EXTERNAL_STORAGE permission until the policy is updated. If targeting Android 10, the requestLegacyExternalStorage flag will still be available for use [30].

[30]https://support.google.com/googleplay/android-developer/answer/9956427?hl=en
MediaStore API

The following have been newly added based on the application as APIs to access the media collections (photos, videos, audio).

Table 4.6.8 New MediaStore APIs
createWriteRequest() Request that the user grant the app write access to the specified group of media files
createFavoriteRequest() Request that the user marks the specified media files as some of their “favorite” media on the device
createTrashRequest() Request that the user place the specified media files in the device’s trash
createDeleteRequest() Request that the user permanently delete the specified media files, without placing them in the trash beforehand

4.7. Using Browsable Intent

Android application can be designed to launch from browser corresponding with a webpage link. This functionality is called ‘Browsable Intent.’ By specifying URI scheme in Manifest file, an application responds the transition to the link (user tap etc.) which has its URI scheme, and the application is launched with the link as a parameter.

In addition, the method to launch the corresponding application from browser by using URI scheme is supported not only in Android but also in iOS and other platforms, and this is generally used for the linkage between Web application and external application, etc. For example, following URI scheme is defined in Twitter application or Facebook application, and the corresponding applications are launched from the browser both in Android and in iOS.

Table 4.7.1 URI scheme and Corresponding application
URI scheme Corresponding application
fb:// Facebook
twitter:// Twitter

It seems very convenient function considering the linkage and convenience, but there are some risks that this function is abused by a malicious third party. What can be supposed are as follows, they abuse application functions by preparing a malicious Web site with a link in which URL has incorrect parameter, or they get information which is included in URL by cheating a smartphone owner into installing the Malware which responds the same URI scheme.

There are some points to be aware when using ‘Browsable Intent’ against these risks.

4.7.1. Sample Code

Sample codes of an application which uses ‘Browsable Intent’ are shown below. Install ‘Starter.html’ on the web server and run it.

Points:

  1. (Webpage side) Sensitive information must not be included.
  2. Handle the URL parameter carefully and securely.
Starter.html
<html>
    <body>
        <!-- *** POINT 1 *** Sensitive information must not be included. -->
        <!-- Character strings to be passed as URL parameter, should be UTF-8 and URI encoded. -->
        <a href="secure://jssec?user=user_id"> Login </a>
    </body>
</html>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.browsableintent" >

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:allowBackup="false" >
    <activity
        android:name=".BrowsableIntentActivity"
        android:label="@string/title_activity_browsable_intent"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>

      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <!-- Accept implicit Intent -->
        <category android:name="android.intent.category.DEFAULT" />
        <!-- Accept Browsable intent -->
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accept URI 'secure://jssec' -->
        <data android:scheme="secure" android:host="jssec"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
BrowsableIntentActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.browsableintent;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.TextView;

public class BrowsableIntentActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_browsable_intent);

        Intent intent = getIntent();
        Uri uri = intent.getData();
        if (uri != null) {
            // Get UserID which is passed by URI parameter
            // *** POINT 2 *** Handle the URL parameter carefully and securely.
            // Omitted, since this is a sample. Please refer to
            // "3.2 Handling Input Data Carefully and Securely."
            String userID = "User ID = " +  uri.getQueryParameter("user");
            TextView tv = (TextView)findViewById(R.id.text_userid);
            tv.setText(userID);
        }
    }

}

4.7.2. Rule Book

Follow rules listed below when using “Browsable Intent”.

  1. (Webpage side) Sensitive Information Must Not Be Included in Parameter of Corresponding Link (Required)
  2. Handle the URL Parameter Carefully and Securely (Required)

4.7.2.2. Handle the URL Parameter Carefully and Securely (Required)

URL parameters which are sent to an application are not always from a legitimate Web page, since a link which is matched with URI scheme can be made by not only developers but anyone. In addition, there is no method to verify whether the URL parameter is sent from a valid Web page or not.

So it is necessary to verify safety of a URL parameter before using it, e.g. check if an unexpected value is included or not.

4.8. Outputting Log to LogCat

There’s a logging mechanism called LogCat in Android, and not only system log information but also application log information are also output to LogCat. Log information in LogCat can be read out from other application in the same device [31], so the application which outputs sensitive information to Logcat, is considered that it has the vulnerability of the information leakage. The sensitive information should not be output to LogCat.

[31]The log information output to LogCat can be read by applications that declare using READ_LOGS permission. However, in Android 4.1 and later, log information that is output by other application cannot be read. But smartphone user can read every log information output to logcat through ADB.

From a security point of view, in release version application, it’s preferable that any log should not be output. However, even in case of release version application, log is output for some reasons in some cases. In this chapter, we introduce some ways to output messages to LogCat in a safe manner even in a release version application. Along with this explanation, please refer to “4.8.3.1. Two Ways of Thinking for the Log Outputting in Release version application”.

4.8.1. Sample Code

Herein after, the method to control the Log output to LogCat by ProGuard in release version application. ProGuard is one of the optimization tools which automatically delete the unnecessary code like unused methods, etc.

There are five types of log output methods, Log.e(), Log.w(), Log.i(), Log.d(), Log.v(), in android.util.Log class. Regarding log information, intentionally output log information (hereinafter referred to as the Operation log information) should be distinguished from logging which is inappropriate for a release version application such as debug log (hereinafter referred to as the Development log information). It’s recommended to use Log.e()/w()/i() for outputting operation log information, and to use Log.d()/v() for outputting development log. Refer to “4.8.3.2. Selection Standards of Log Level and Log Output Method” for the details of proper usage of five types of log output methods, in addition, also refer to “4.8.3.3. DEBUG Log and VERBOSE Log Are Not Always Deleted Automatically”.

here’s an example of how to use LogCat in a safe manner. This example includes Log.d() and Log.v() for outputting debug log. If the application is for release, these two methods would be deleted automatically. In this sample code, ProGuard is used to automatically delete code blocks where Log.d()/v() is called.

Points:

  1. Sensitive information must not be output by Log.e()/w()/i(), System.out/err.
  2. Sensitive information should be output by Log.d()/v() in case of need.
  3. The return value of Log.d()/v() should not be used (with the purpose of substitution or comparison).
  4. When you build an application for release, you should bring the mechanism that automatically deletes inappropriate logging method like Log.d() or Log.v() in your code.
  5. An APK file for the (public) release must be created in release build configurations.
ProGuardActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.log.proguard;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class ProGuardActivity extends Activity {

    final static String LOG_TAG = "ProGuardActivity";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_proguard);

        // *** POINT 1 *** Sensitive information must not be output by
        // Log.e()/w()/i(), System.out/err.
        Log.e(LOG_TAG, "Not sensitive information (ERROR)");
        Log.w(LOG_TAG, "Not sensitive information (WARN)");
        Log.i(LOG_TAG, "Not sensitive information (INFO)");

        // *** POINT 2 *** Sensitive information should be output by
        // Log.d()/v() in case of need.
        // *** POINT 3 *** The return value of Log.d()/v()should not be used
        // (with the purpose of substitution or comparison).
        Log.d(LOG_TAG, "sensitive information (DEBUG)");
        Log.v(LOG_TAG, "sensitive information (VERBOSE)");
    }
}
proguard-project.txt
# prevent from changing class name and method name etc.
-dontobfuscate

# *** POINT 4 *** In release build, the build configurations in which Log.d()/v()
# are deleted automatically should be constructed.
-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

*** Point 5 *** An APK file for the (public) release must be created in release build configurations.

_images/image49.png

Fig. 4.8.1 How to create release version application

The difference of LogCat output between development version application (debug build) and release version application (release build) are shown in below Fig. 4.8.2

_images/image50.png

Fig. 4.8.2 Difference of LogCat output between development version application and release version application

4.8.2. Rule Book

When you output log messages, follow the rules below.

  1. Sensitive Information Must Not Be Included in Operation Log Information (Required)
  2. Construct the Build System to Auto-delete Codes which Output Development Log Information When Build for the Release (Recommended)
  3. Use Log.d()/v() Method When Outputting Throwable Object (Recommended)
  4. Use Only Methods of the android.util.Log Class for the Log Output (Recommended)

4.8.2.1. Sensitive Information Must Not Be Included in Operation Log Information (Required)

Log which was output to LogCat can be read out from other applications, so sensitive information like user’s login information should not be output by release version application. It’s necessary not to write code which outputs sensitive information to log during development, or it’s necessary to delete all of such codes before release.

To follow this rule, first, not to include sensitive information in operation log information. In addition, it’s recommended to construct the system to delete code which outputs sensitive information when build for release. Please refer to “4.8.2.2. Construct the Build System to Auto-delete Codes which Output Development Log Information When Build for the Release (Recommended)”.

4.8.2.2. Construct the Build System to Auto-delete Codes which Output Development Log Information When Build for the Release (Recommended)

When application development, sometimes it’s preferable if sensitive information is output to log for checking the process contents and for debugging, for example the interim operation result in the process of complicated logic, information of program’s internal state, communication data structure of communication protocol. It doesn’t matter to output the sensitive information as debug log during developing, in this case, the corresponding log output code should be deleted before release, as mentioned in “4.8.2.1. Sensitive Information Must Not Be Included in Operation Log Information (Required)”.

To delete surely the code which outputs development log information when release builds, the system which executes code deletion automatically by using some tools, should be constructed. ProGuard, which was described in “4.8.1. Sample Code”, can work for this method. As described below, there are some noteworthy points on deleting code by ProGuard. Here it’s supposed to apply the system to applications which output development log information by either of Log.d()/v(), based on “4.8.3.2. Selection Standards of Log Level and Log Output Method”.

ProGuard deletes unnecessary code like unused methods, automatically. By specifying Log.d()/v() as parameter of -assumenosideeffects option, call for Log.d(), Log.v() are granted as unnecessary code, and those are to be deleted.

By specifying -assumenosideeffects to Log.d()/v(), make it auto-deletion target.

-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

In case using this auto deletion system, pay attention that Log.v()/d() code is not deleted when using returned value of Log.v(), Log.d(), so returned value of Log.v(), Log.d(), should not be used. For example, Log.v() is not deleted in the next examination code.

Examination code which Log.v() that is specifeied to be deleted is not deketed.

    int i = android.util.Log.v("tag", "message");
    //Use the returned value of Log.v() for examination.
    System.out.println(String.format("Log.v() returned %d.", i));

If you’d like to reuse source code, you should keep the consistency of the project environment including ProGuard settings. For example, source code that presupposes Log.d() and Log.v() are deleted automatically by above ProGuard setting. If using this source code in another project which ProGuard is not set, Log.d() and Log.v() are not to be deleted, so there’s a risk that the sensitive information may be leaked. When reusing source code, the consistency of project environment including ProGuard setting should be secured.

4.8.2.3. Use Log.d()/v() Method When Outputting Throwable Object (Recommended)

As mentioned in “4.8.1. Sample Code” and “4.8.3.2. Selection Standards of Log Level and Log Output Method”, sensitive information should not be output to log through Log.e()/w()/i(). On the other hand, in order that a developer wants to output the details of program abnormality to log, when exception occurs, stack trace is output to LogCat by Log.e(…, Throwable tr)/w(…, Throwable tr)/i(…, Throwable tr), in some cases. However, sensitive information may sometimes be included in the stack trace because it shows detail internal structure of the program. For example, when SQLiteException is output as it is, what type of SQL statement is issued is clarified, so it may give the clue for SQL injection attack. Therefore, it’s recommended that use only Log.d()/Log.v() methods, when outputting throwable object.

4.8.2.4. Use Only Methods of the android.util.Log Class for the Log Output (Recommended)

You may output log by System.out/err to verify the application’s behavior whether it works as expected or not, during development. Of course, log can be output to LogCat by print()/println() method of System.out/err, but it’s strongly recommended to use only methods of android.util.Log class, by the following reasons.

When outputting log, generally, use the most appropriate output method properly based on the urgency of the information, and control the output. For example, categories like serious error, caution, simple application’s information notice, etc. are to be used. However, in this case, information which needs to be output at the time of release (operation log information) and information which may include the sensitive information (development log information) are output by the same method. So, it may happen that when delete code which outputs sensitive information, it’s in danger that some deletion are dropped by oversight.

Along with this, when using android.util.Log and System.out/err for log output, compared with using only android.util.Log, what needs to be considered will increase, so it’s in danger that some mistakes may occur, like some deletion are dropped by oversight.

To decrease risk of above mentioned mistakes occurrence, it’s recommended to use only methods of android.util.Log class.

4.8.3. Advanced Topics

4.8.3.1. Two Ways of Thinking for the Log Outputting in Release version application

There are two ways of thinking for log output in release version application. One is any log should never be output, and another is necessary information for later analysis should be output as log. It’s favorable that any log should never be output in release version application from the security point of view, but sometimes, log is output even in release version application for various reasons. Each way of thinking is described as per below.

The former is “Any log should never be output”, this is because outputting log in release version application is not so much valuable, and there is a risk to leak sensitive information. This comes from there’s no method for developers to collect log information of the release version application in Android application operation environment, which is different from many Web application operation environments. Based on this thinking, the logging codes are used only in development phase, and all the logging codes are deleted on building release version application.

The latter is “necessary information should be output as log for the later analysis”, as a final option to analyze application bugs in customer support, in case of any questions or doubt to your customer support. Based on this idea, as introduced above, it is necessary to prepare the system that prevent human errors and bring it in your project because if you don’t have the system you have to keep in mind to avoid logging the sensitive information in release version application.

For more details about logging method, refer to the following document.

Code Style Guidebook for Contributors / Log Sparingly

4.8.3.2. Selection Standards of Log Level and Log Output Method

There are five levels of log level (ERROR, WARN, INFO, DEBUG, VERBOSE) are defined in android.util.Log class in Android. You should select the most appropriate method when using the android.util.Log class to output log messages according to Table 4.8.1 which shows the selection standards of logging levels and methods.

Table 4.8.1 Selection standards of log levels and log output method
Log level Method Log information to be output Cautions for application release
ERROR Log.e() Log information which is output when application is in a fatal state. Log information as per left may be referred by users, so it could be output both in development version application and in release version application. Therefore, sensitive information should not be output in these levels.
WARN Log.w() Log information which is output when application faces the unexpected serious situation.
INFO Log.i() Other than above, log information which is output to notify any remarkable changes or results in application state.
DEBUG Log.d() Program’s internal state information which needs to be output temporarily for analyzing the cause of specific bug when developing application. Log information as per left is only for application developers. Therefore, this type of information should not be output in case of release version application.
VERBOSE Log.v() Log information which is not applied to any of above. Log information which application developer outputs for many purposes, is applied this. For example, in case of outputting server communication data to dump.

For more details about logging method, refer to the following document.

Code Style Guidelines for Contributors / Log Sparingly

4.8.3.3. DEBUG Log and VERBOSE Log Are Not Always Deleted Automatically

The following is quoted from the developer reference of android.util.Log class [32].

The order in terms of verbosity, from least to most is ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled into an application except during development. Debug logs are compiled in but stripped at runtime. Error, warning and info logs are always kept.

[32]https://developer.android.com/reference/android/util/Log.html

After reading the above texts, some developers might have misunderstood the Log class behavior as per below.

  • Log.v() call is not compiled when release build, VERBOSE log is never output.
  • Log.v() call is compiled, but DEBUG log is never output when execution.

However, logging methods never behave in above ways, and all messages are output regardless of whether it is compiled with debug mode or release mode. If you read the document carefully, you will be able to realize that the gist of the document is not about the behavior of logging methods but basic policies for logging.

In this chapter, we introduced the sample code to get the expected result as described above by using ProGuard.

4.8.3.4. Remove Sensitive Information from Assembly

If you build the following code with ProGuard for the purpose of deleting Log.d() method, it is necessary to remember that ProGuard keeps the statement that construct the string for logging message (the first line of the code) even though it remove the statement of calling Log.d() method (the second line of the code).

    String debug_info = String.format("%s:%s",
                                      "Sensitive information 1",
                                      "Sensitive information 2");
    if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);

The following disassembly shows the result of release build of the code above with ProGuard. Actually, there’s no Log.d() call process, but you can see that character string consistence definition like “Sensitive information1” and calling process of String#format() method, are not deleted and still remaining there.

    const-string v1, "%s:%s"
    const/4 v2, 0x2
    new-array v2, v2, [Ljava/lang/Object;
    const/4 v3, 0x0
    const-string v4, "Sensitive information 1"
    aput-object v4, v2, v3
    const/4 v3, 0x1
    const-string v4, "Sensitive information 2"
    aput-object v4, v2, v3
    invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
    move-result-object v0

Actually, it’s not easy to find the particular part that disassembled APK file and assembled log output information as above. However, in some application which handles the very confidential information, this type of process should not be remained in APK file in some cases.

You should implement your application like below to avoid such a consequence of remaining the sensitive information in bytecode [33]. In release build, the following codes are deleted completely by the compiler optimization.

[33]The previous sample code is enclosed in an if statement with BuildConfig.DEBUG as conditional expression. The if statement before the call to Log.d () is not necessary, but left as it is for comparison with the previous one.
    if (BuildConfig.DEBUG) {
        String debug_info = String.format("%s:%s",
                                          "Sensitive information 1",
                                          "Sensitive information 2");
        if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);
    }

Besides, ProGuard cannot remove the log message of the following code(”result:” + value).

    Log.d(TAG, "result:" + value);

In this case, you can solve the problem in the following manner.

    if (BuildConfig.DEBUG) Log.d(TAG, "result:" + value);

4.8.3.5. The Contents of Intent Is Output to LogCat

When using Activity, it’s necessary to pay attention, since ActivityManager outputs the content of Intent to LogCat. Refer to “4.1.3.5. Log Output When using Activities”.

4.8.3.6. Restrain Log which Is Output to System.out/err

System.out/err method outputs all messages to LogCat. Android could send some messages to System.out/err even if developers did not use these methods in their code, for example, in the following cases, Android sends stack trace to System.err method.

  • When using Exception#printStackTrace()
  • When it’s output to System.err implicitly
    (When the exception is not caught by application, it’s given to Exception#printStackTrace() by the system.)

You should handle errors and exceptions appropriately since the stack trace includes the unique information of the application.

We introduce a way of changing default output destination of System.out/err. The following code redirects the output of System.out/err method to nowhere when you build a release version application. However, you should consider whether this redirection does not cause a malfunction of application or system because the code temporarily overwrites the default behavior of System.out/err method. Furthermore, this redirection is effective only to your application and is worthless to system processes.

OutputRedirectApplication.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.log.outputredirection;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;

import android.app.Application;

public class OutputRedirectApplication extends Application {

    // PrintStream which is not output anywhere
    private final PrintStream emptyStream = new PrintStream(new OutputStream() {
            public void write(int oneByte) throws IOException {
                // do nothing
            }
        });

    @Override
    public void onCreate() {
        // Redirect System.out/err to PrintStream which doesn't output anywhere,
        // when release build.

        // Save original stream of System.out/err
        PrintStream savedOut = System.out;
        PrintStream savedErr = System.err;

        // Once, redirect System.out/err to PrintStream which doesn't output
        // anywhere
        System.setOut(emptyStream);
        System.setErr(emptyStream);

        // Restore the original stream only when debugging. (In release build,
        // the following 1 line is deleted byProGuard.)
        resetStreams(savedOut, savedErr);
    }

    // All of the following methods are deleted byProGuard when release.
    private void resetStreams(PrintStream savedOut, PrintStream savedErr) {
        System.setOut(savedOut);
        System.setErr(savedErr);
    }
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.log.outputredirection" >

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:name=".OutputRedirectApplication"
      android:allowBackup="false" >
    <activity
        android:name=".LogActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>
proguard-project.txt
# Prevent from changing class name and method name, etc.
-dontobfuscate

# In release build, delete call from Log.d()/v() automatically.
-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

# In release build, delete resetStreams() automatically.
-assumenosideeffects class
    org.jssec.android.log.outputredirection.OutputRedirectApplication {
    private void resetStreams(...);
}

The difference of LogCat output between development version application (debug build) and release version application (release build) are shown as per below Fig. 4.8.3.

_images/image51.png

Fig. 4.8.3 Difference of System.out/err in LogCat output, between development application and release application.

4.9. Using WebView

WebView enables your application to integrate HTML/JavaScript content.

4.9.1. Sample Code

We need to take proper action, depending on what we’d like to show through WebView although we can easily show web site and html file by it. And also we need to consider risk from WebView’s remarkable function; such as JavaScript-Java object bind.

Especially what we need to pay attention is JavaScript. (Please note that JavaScript is disabled as default. And we can enable it by WebSettings#setJavaScriptEnabled()). With enabling JavaScript, there is potential risk that malicious third party can get device information and operate your device.

The following is principle for application with WebView [34]:

  1. You can enable JavaScript if the application uses contents which are managed in house.
  2. You should NOT enable JavaScript other than the above case.
[34]Strictly speaking, you can enable JavaScript if we can say the content is safe. If the contents are managed in house, the contents should be guaranteed of security. And the company can secure them. In other words, we need to have business representation’s decision to enable JavaScript for other company’s contents. The contents which are developed by trusted partner might have security guarantee. But there is still potential risk. Therefore the decision is needed by responsible person.

Fig. 4.9.1 shows flow chart to choose sample code according to content characteristic.

_images/image52.png

Fig. 4.9.1 Flow Figure to select Sample code of WebView

4.9.1.1. Show Only Contents Stored under assets/res Directory in the APK

You can enable JavaScript if your application shows only contents stored under assets/ and res/ directory in apk.

The following sample code shows how to use WebView to show contents stored under assets/ and res/.

Points:

  1. Disable to access files (except files under assets/ and res/ in apk).
  2. You may enable JavaScript.
WebViewAssetsActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.webview.assets;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;

public class WebViewAssetsActivity extends Activity {
    /**
     * Show contents in assets
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = (WebView) findViewById(R.id.webView);
        WebSettings webSettings = webView.getSettings();

        // *** POINT 1 *** Disable to access files (except files under assets/
        // and res/ in this apk)
        webSettings.setAllowFileAccess(false);

        // *** POINT 2 *** Enable JavaScript (Optional)
        webSettings.setJavaScriptEnabled(true);

        // Show contents which were stored under assets/ in this apk
        webView.loadUrl("file:///android_asset/sample/index.html");
    }
}

4.9.1.2. Show Only Contents which Are Managed In-house

You can enable JavaScript to show only contents which are managed in-house only if your web service and your Android application can take proper actions to secure both of them.

  • Web service side actions:

As Fig. 4.9.2 shows, your web service can only refer to contents which are managed in-house. In addition, the web service is needed to take appropriate security action. Because there is potential risk if contents which your web service refers to may have risk; such as malicious attack code injection, data manipulation, etc.

Please refer to “4.9.2.1. Enable JavaScript Only If Contents Are Managed In-house (Required)”.

  • Android application side actions:

Using HTTPS, the application should establish network connection to your managed web service only if the certification is trusted.

The following sample code is an activity to show contents which are managed in-house.

_images/image53.png

Fig. 4.9.2 Accessible contents and Non-accessible contents from application.

Points:

  1. Handle SSL error from WebView appropriately.
  2. (Optional) Enable JavaScript of WebView.
  3. Restrict URLs to HTTPS protocol only.
  4. Restrict URLs to in-house.
WebViewTrustedContentsActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.webview.trustedcontents;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Bundle;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import java.text.SimpleDateFormat;

public class WebViewTrustedContentsActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        WebView webView = (WebView) findViewById(R.id.webView);

        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onReceivedSslError(WebView view,
                                           SslErrorHandler handler,
                                           SslError error) {
                // *** POINT 1 *** Handle SSL error from WebView appropriately
                // Show SSL error dialog.
                AlertDialog dialog = createSslErrorDialog(error);
                dialog.show();

                // *** POINT 1 *** Handle SSL error from WebView appropriately
                // Abort connection in case of SSL error
                // Since, there may be some defects in a certificate like
                // expiration of validity, or it may be man-in-the-middle attack.
                handler.cancel();
            }
        });

        // *** POINT 2 *** Enable JavaScript (optional) 
        // in case to show contents which are managed in house.
        webView.getSettings().setJavaScriptEnabled(true);

        // *** POINT 3 *** Restrict URLs to HTTPS protocol only
        // *** POINT 4 *** Restrict URLs to in-house
        webView.loadUrl("https://url.to.your.contents/");
    }

    private AlertDialog createSslErrorDialog(SslError error) {
        // Error message to show in this dialog
        String errorMsg = createErrorMessage(error);
        // Handler for OK button
        DialogInterface.OnClickListener onClickOk =
            new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                setResult(RESULT_OK);
            }
        };
        // Create a dialog
        AlertDialog dialog = new AlertDialog.Builder
            (WebViewTrustedContentsActivity.this).setTitle("SSL connection error")
            .setMessage(errorMsg).setPositiveButton("OK", onClickOk)
            .create();
        return dialog;
    }

    private String createErrorMessage(SslError error) {
        SslCertificate cert = error.getCertificate();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        StringBuilder result = new StringBuilder().append("The site's certification is NOT valid. Connection was disconnected.\n\nError:\n");
        switch (error.getPrimaryError()) {
        case SslError.SSL_EXPIRED:
            result.append("The certificate is no longer valid.\n\nThe expiration date is ").append(dateFormat.format(cert.getValidNotAfterDate()));
            return result.toString();
        case SslError.SSL_IDMISMATCH:
            result.append("Host name doesn't match. \n\nCN=").append(cert.getIssuedTo().getCName());
            return result.toString();
        case SslError.SSL_NOTYETVALID:
            result.append("The certificate isn't valid yet.\n\nIt will be valid from ").append(dateFormat.format(cert.getValidNotBeforeDate()));
            return result.toString();
        case SslError.SSL_UNTRUSTED:
            result.append("Certificate Authority which issued the certificate is not reliable.\n\nCertificate Authority\n").append(cert.getIssuedBy().getDName());
            return result.toString();
        default:
            result.append("Unknown error occured. ");
            return result.toString();

        }
    }
}

4.9.1.3. Show Contents which Are Not Managed In-house

Don’t enable JavaScript if your application shows contents which are not managed in house because there is potential risk to access to malicious content.

The following sample code is an activity to show contents which are not managed in-house.

This sample code shows contents specified by URL which user inputs through address bar. Please note that JavaScript is disabled and connection is aborted when SSL error occurs. The error handling is the same as “4.9.1.2. Show Only Contents which Are Managed In-house” for the details of HTTPS communication. Please refer to “5.4. Communicating via HTTPS” for the details also.

Points:

  1. Handle SSL error from WebView appropriately.
  2. Disable JavaScript of WebView.
WebViewUntrustActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.webview.untrust;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Bundle;
import android.view.View;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;

import java.text.SimpleDateFormat;

public class WebViewUntrustActivity extends Activity {
    /*
     * Show contents which are NOT managed in-house (Sample program works as a
     * simple browser)
     */

    private EditText textUrl;
    private Button buttonGo;
    private WebView webView;

    // Activity definition to handle any URL request 
    private class WebViewUnlimitedClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView webView, String url) {
            webView.loadUrl(url);
            textUrl.setText(url);
            return true;
        }

        // Start reading Web page 
        @Override
        public void onPageStarted(WebView webview, String url, Bitmap favicon) {
            buttonGo.setEnabled(false);
            textUrl.setText(url);
        }

        // Show SSL error dialog
        // And abort connection. 
        @Override
        public void onReceivedSslError(WebView webview,
                                       SslErrorHandler handler, SslError error) {
                        
            // *** POINT 1 *** Handle SSL error from WebView appropriately
            AlertDialog errorDialog = createSslErrorDialog(error);
            errorDialog.show();
            handler.cancel();
            textUrl.setText(webview.getUrl());
            buttonGo.setEnabled(true);
        }

        // After loading Web page, show the URL in EditText.
        @Override
        public void onPageFinished(WebView webview, String url) {
            textUrl.setText(url);
            buttonGo.setEnabled(true);
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        webView = (WebView) findViewById(R.id.webview);
        webView.setWebViewClient(new WebViewUnlimitedClient());

        // *** POINT 2 *** Disable JavaScript of WebView
        // Explicitly disable JavaScript even though it is disabled by default.
        webView.getSettings().setJavaScriptEnabled(false);

        webView.loadUrl(getString(R.string.texturl));
        textUrl = (EditText) findViewById(R.id.texturl);
        buttonGo = (Button) findViewById(R.id.go);
    }

    public void onClickButtonGo(View v) {
        webView.loadUrl(textUrl.getText().toString());
    }

    private AlertDialog createSslErrorDialog(SslError error) {
        // Error message to show in this dialog
        String errorMsg = createErrorMessage(error);
        // Handler for OK button
        DialogInterface.OnClickListener onClickOk =
            new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                setResult(RESULT_OK);
            }
        };
        // Create a dialog
        AlertDialog dialog =
            new AlertDialog.Builder(WebViewUntrustActivity.this)
                .setTitle("SSL connection error")
                .setMessage(errorMsg).setPositiveButton("OK", onClickOk)
                .create();
        return dialog;
    }

    private String createErrorMessage(SslError error) {
        SslCertificate cert = error.getCertificate();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        StringBuilder result = new StringBuilder().append("The site's certification is NOT valid. Connection was disconnected.\n\nError:\n");
        switch (error.getPrimaryError()) {
        case SslError.SSL_EXPIRED:
            result.append("The certificate is no longer valid.\n\nThe expiration date is ").append(dateFormat.format(cert.getValidNotAfterDate()));
            return result.toString();
        case SslError.SSL_IDMISMATCH:
            result.append("Host name doesn't match. \n\nCN=").append(cert.getIssuedTo().getCName());
            return result.toString();
        case SslError.SSL_NOTYETVALID:
            result.append("The certificate isn't valid yet.\n\nIt will be valid from ").append(dateFormat.format(cert.getValidNotBeforeDate()));
            return result.toString();
        case SslError.SSL_UNTRUSTED:
            result.append("Certificate Authority which issued the certificate is not reliable.\n\nCertificate Authority\n").append(cert.getIssuedBy().getDName());
            return result.toString();
        default:
            result.append("Unknown error occured. ");
            return result.toString();
        }
    }
}

4.9.2. Rule Book

Comply with following rule when you need to use WebView.

  1. Enable JavaScript Only If Contents Are Managed In-house (Required)
  2. Use HTTPS to Communicate to Servers which Are Managed In-house (Required)
  3. Verify That the URL Received from Others Such as Through Intent Is the Expected URL (Required)
  4. Handle SSL Error Properly (Required)

4.9.2.1. Enable JavaScript Only If Contents Are Managed In-house (Required)

What we have to pay attention on WebView is whether we enable the JavaScript or not. As principle, we can only enable the JavaScript only IF the application will access to services which are managed in-house. And you must not enable the JavaScript if there is possibility to access services which are not managed in-house.

Services managed In-house

In case that application accesses contents which are developed IN HOUSE and are distributed through servers which are managed IN HOUSE, we can say that the contents are ONLY modified by your company. In addition, it is also needed that each content refers to only contents stored in the servers which have proper security.

In this scenario, we can enable JavaScript on the WebView. Please refer to “4.9.1.2. Show Only Contents which Are Managed In-house” also.

And you can also enable JavaScript if your application shows only contents stored under assets/ and res/ directory in the apk. Please refer to “4.9.1.1. Show Only Contents Stored under assets/res Directory in the APK” also.

Services unmanaged in-house

You must NOT think you can secure safety on contents which are NOT managed IN HOUSE. Therefore you have to disable JavaScript. Please refer to “4.9.1.3. Show Contents which Are Not Managed In-house”.

In addition, you have to disable JavaScript if the contents are stored in external storage media; such as microSD because other application can modify the contents.

4.9.2.2. Use HTTPS to Communicate to Servers which Are Managed In-house (Required)

You have to use HTTPS to communicate to servers which are managed in-house because there is potential risk of spoofing the services by malicious third party.

Please refer to both “4.9.2.4. Handle SSL Error Properly (Required)”, and “5.4. Communicating via HTTPS”.

4.9.2.3. Verify That the URL Received from Others Such as Through Intent Is the Expected URL (Required)

Implementation to receive an Intent from other application and display the URL provided in the Intent parameter on the WebView is common on many applications. If the URL provided at this point is to be displayed without verifying the expected URL, malicious websites such as phishing websites may be displayed on the WebView. The problem with this implementation is that unspecified URLs that are not guaranteed to be safe may become displayed. This may result in damages even if the JavaScript of the WebView is disabled.

To verify that the URL provided as the parameter of the Intent is the expected URL, there is a method to store the URL whitelist to display in the application in advance. Safety can be secured by displaying only the URL that matches this whitelist on the WebView. In addition, the URL to be registered to the whitelist must be HTTPS.

WebViewActivity.java
    // Get the white list from the resource file
    String[] allowList = getResources().getStringArray(R.array.allow_url_list);

    // Check the URL domain received from Intent is included the white list
    Uri uri = Uri.parse("the URL received from Intent");
    for (String str : allowList) {
        if (uri.getScheme().equals("https") && uri.getHost().equals(str)) {
            webView.loadUrl(uri.toString());
    }

To show the received URL on a WebView with JavaScript enabled, you must additionally verify that this URL is managed in-house.

Sample code in the section “4.9.1.2. Show Only Contents which Are Managed In-house” uses the fixed value URL to show contents which are managed in-house, to secure safety.

4.9.2.4. Handle SSL Error Properly (Required)

You have to terminate the network communication and inform error notice to user when SSL error happens on HTTPS communication.

SSL error shows invalid server certification risk or MTIM (man-in-the-middle attack) risk. Please note that WebView has NO error notice mechanism regarding SSL error. Therefore your application has to show the error notice to inform the risk to the user. Please refer to sample code in the section of “4.9.1.2. Show Only Contents which Are Managed In-house”, and “4.9.1.3. Show Contents which Are Not Managed In-house”.

In addition, your application MUST terminate the communication with the error notice.

In other words, you MUST NOT do following.

  • Ignore the error to keep the transaction with the service.
  • Retry HTTP communication instead of HTTPS.

Please refer to the detail described in “5.4. Communicating via HTTPS”.

WebView’s default behavior is to terminate the communication in case of SSL error. Therefore what we need to add is to show SSL error notice. And then we can handle SSL error properly.

4.9.3. Advanced Topics

4.9.3.1. Vulnerability caused by addJavascriptInterface() at Android versions 4.1 or earlier

Android versions under 4.2(API Level 17) have a vulnerability caused by addJavascriptInterface(), which could allow attackers to call native Android methods (Java) via JavaScript on WebView.

As explained in “4.9.2.1. Enable JavaScript Only If Contents Are Managed In-house (Required)”, JavaScript must not be enabled if the services could access services out of in-house control.

In Android 4.2(API Level 17) or later, the measure of the vulnerability has been taken to limit access from JavaScript to only methods with @JavascriptInterface annotation on Java source codes instead of all methods of Java objects injected. However it is necessary to disable JavaScript if the services could access services out of in-house control as mentioned in “4.9.2.1.”.

4.9.3.2. Issue caused by file scheme

In case of using WebView with default settings, all files that the app has access rights can be accessed to by using the file scheme in web pages regardless of the page origins. For example, a malicious web page could access the files stored in the app’s private directory by sending a request to the uri of a private file of the app with the file scheme.

A countermeasure is to disable JavaScript as explained in “4.9.2.1. Enable JavaScript Only If Contents Are Managed In-house (Required)” if the services could access services out of in-house control. Doing that is to protect against sending the malicious file scheme request.

Also in case of Android 4.1 (API Level 16) or later, setAllowFileAccessFromFileURLs() and setAllowUniversalAccessFromFileURLs() can be used to limit access via the file scheme.

Disabling the file scheme

        webView = (WebView) findViewById(R.id.webview);
        webView.setWebViewClient(new WebViewUnlimitedClient());
        WebSettings settings = webView.getSettings();
        settings.setAllowUniversalAccessFromFileURLs(false);
        settings.setAllowFileAccessFromFileURLs(false);

4.9.3.3. Specifying a Sender Origin When Using Web Messaging

Android 6.0 (API Level 23) adds an API for realizing HTML5 Web Messaging. Web Messaging is a framework defined in HTML5 for sending and receiving data between different browsing contexts [35].

[35]https://www.w3.org/TR/webmessaging/

The postWebMessage() method added to the WebView class is a method for processing data transmissions via the Cross-domain messaging protocol defined by Web Messaging.

This method sends a message object—specified by its first parameter—from the browsing context that has been read into WebView; however, in this case it is necessary to specify the origin of the sender as the second parameter. If the specified origin [36] does not agree with the origin in the sender context, the message will not be sent. By placing restrictions on the sender origin in this way, this mechanism aims to prevent the passing of messages to unintended senders.

[36]An “origin” is a URL scheme together with a host name and port number. For the detailed definition see http://tools.ietf.org/html/rfc6454.

However, it is important to note that wildcards may be specified as the origin in the postWebMessage() method [37]. If wildcards are specified, the sender origin of the message is not checked, and the message may be sent from any arbitrary origin. In a situation in which malicious content has been read into WebView, various types of harm or damage may result if important messages are sent without origin restrictions. Thus, when using WebView for Web messaging, it is best to specify explicitly a specific origin in the postWebMessage() method.

[37]Note that Uri.EMPTY and Uri.parse(“”) function as wildcards (at the time of writing the September 1, 2016 version).

4.9.3.4. Safe Browsing in WebView

Safe Browsing is a service provided by Google that displays a warning page when the user tries to access a malware page, phishing site, or other unsafe web page.

_images/image94.png

Fig. 4.9.3 Warning page displayed when attempting to access an unsafe web page in Chrome for Android

Currently, the Safe Browsing function can be used not only in Chrome for Android and other browser applications, but also in the WebView used in applications. However, careful attention is needed because the components that can be used for WebView vary depending on the Android OS version of the system, as a result, the degree of support for Safe Browsing also varies. Support for standard WebView and Safe Browsing by Android OS versions are shown in the following table.

Table 4.9.1 Android OS version and standard WebView support
Android OS version Android standard WebView Relation with OS Adapting to Safe Browsing
Android 7.0 or later Chrome for Android (Chromium base) Independent OK
Android 5.0 - 6.0 Android System WebView (Chromium base) Independent OK
Android 4.4 OS embedded WebView (Chromium base) Embedded No
Android 4.3 or earlier OS embedded WebView Embedded No

Before Android 4.3 (API level 18), a WebView that did not include the Safe Browsing function was incorporated into the OS, and this was changed in Android 4.4 (API level 19) so that WebView included the Safe Browsing function. Even so, care is needed because the version is old, and it does not support use of the Safe Browsing function in the WebView of applications.

The capability to use the Safe Browsing function in applications started from Android 5.0 (API level 21) when WebView was separated from the OS and became updated as an application.

Starting from WebView 66, Safe Browsing is enabled by default, and no special settings are required at the application side. However, it is possible that Safe Browsing may not be enabled by default for some WebView versions if the user did not update WebView or if the standard WebView in the “Set WebView implementation” option for developers was changed from the default. And so, if Safe Browsing is used, it must be explicitly enabled as shown below.

Settings for enabling Safe Browsing in AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest package="...">
    <application>
        ...
        <!-- Explicitly elable the Safe Browsing function of WebView in the application process -->
        <meta-data
            android:name="android.webkit.WebView.EnableSafeBrowsing"
            android:value="true" />
    </application>
</manifest>

Also, in Android 8.0 (API level 26), several APIs for Safe Browsing were added.

The setSafeBrowsingEnabled(boolean enabled) added in the WebSettings class is a setting method for dynamically enabling or disabling each WebView instance. Before Android 8.0 (API level 26), the Safe Browsing function was enabled or disabled by settings in AndroidManifest, but this could only make settings for all WebViews in an application. The setSafeBrowsingEnabled(boolean enabled) can be used to allow dynamic enable/disable switching for each WebView instance.

if (url == IN_HOUSE_MANAGEMENT_CONTENT_URL) {
    // (ex.) because in-house contents are detectable by Safe Browsing, 
    // diable it temporarily
    webView.getSettings().setSafeBrowsingEnabled(false); 
} else {
    // normally, it should be enabled
    webView.getSettings().setSafeBrowsingEnabled(true); 
}

Also, in Android 8.1 (API level 27), classes and APIs for Safe Browsing were added. These enable specifying of the Safe Browsing initialization process, settings for responses taken when accessing an unsafe web page, setting of a whitelist for excluding specific sites from Safe Browsing, and more.

The startSafeBrowsing() added in the WebView class is a method that calls the Safe Browsing initialization process for WebView components used for the WebView in applications. The initialization result is passed to the callback object that is passed by the 2nd argument, and so if initialization fails, and false is passed to the callback object, responses such as disabling WebView or not loading the URL are recommended.

// because the Safe Browsing is not supported before Android 8.1,
// real implementations need to check Android OS version of the device
WebView.startSafeBrowsing(this, new ValueCallback<Boolean>() {
    @Override
    public void onReceiveValue(Boolean result) {
        mSafeBrowsingIsInitialized = true;
        if (result) {
            Log.i("WebView SafeBrowsing", "Initialized SafeBrowsing!");
        } else {
            Log.w("WebView SafeBrowsing", "SafeBrowsing initialization failed...");
            // when the initialization failed, Safe Browsing might not work
            // properly in this case, it is advisable to disable WebView
        }
    }
});

Similarly, the setSafeBrowsingWhitelist() added in the WebView class is a method that sets host names and IP addresses that are excluded from Safe Browsing in a whitelist format. When a list of the host names and IP addresses to be excluded from Safe Browsing is passed as an argument, no verification is conducted using Safe Browsing when they are accessed.

// setting the white list of the pair of host name and Ip address which is
// excluded from Safe Browsing
// (ex.) because in-house contents are detectable by Safe Browsing, register
// them to white list
WebView.setSafeBrowsingWhitelist(new ArrayList<>(Arrays.asList( IN_HOUSE_MANAGEMENT_CONTENT_HOSTNAME )),
    new ValueCallback<Boolean>() {
        @Override
        public void onReceiveValue(Boolean aBoolean) {
            Log.i("WebView SafeBrowsing", "Whitelisted " + aBoolean.toString());
        }
    });

The onSafeBrowsingHit() added in the WebClient class is a callback function that is called back when it is determined that a URL accessed in a WebView where Safe Browsing is enabled is an unsafe web page. The object of the WebView that accessed the unsafe web page is passed to the 1st argument, WebResourceRequest is passed to the 2nd argument, the type of threat is passed to the 3rd argument, and the SafeBrowsingResponse object for setting the response when determining that a page is unsafe is passed to the 4th argument.

The response when using the SafeBrowsingResponse object can be selected from the three options below.

  • backToSafety(boolean report): Returns to the previous page without displaying a warning (If no previous page is available, a blank page is displayed.)
  • proceed(boolean report): Ignores the warning and displays the web page.
  • showInterstitial(boolean allowReporting): Displays the warning page (default response)

For backToSafety() and proceed(), an argument can be used to set whether a report is sent to Google, and an argument can be set for showInterstitial() to display “a checkbox for selecting whether a report is sent to Google”.

public class MyWebViewClient extends WebViewClient {
    
    // When Safe Browsing function is enabled, accessing unsafe web page will
    // cause this callback to be ivoked
    @Override
    public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
                                  int threatType, SafeBrowsingResponse callback) {
        // Display warning page with a check box which selects
        // "Send report to Google" or not (Recommended)
        callback.showInterstitial(true);
        // Without displaying warning page, return back to the safe page,
        // and send a report to Google (Recommended)
        callback.backToSafety(true);
        // Ignoring the warning, access to the page, and send a report to
        // Google (Not recommended)
        callback.proceed(false);
    }
}

No Android Support Library is available that supports these classes and APIs. For this reason, to operate applications using these classes and APIs in systems that are below API level 26 or 27, the processes must be separated based on the version or similar measures are required.

Sample code is shown below for handling of access to unsafe web pages when Safe Browsing is used in WebView.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.webview.safebrowsing">

  <uses-permission android:name="android.permission.INTERNET" />

  <application
      android:allowBackup="false"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:theme="@style/AppTheme"
      android:networkSecurityConfig="@xml/network_security_config">
    <activity
        android:name=".MainActivity"
        android:exported="true"
        android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <!-- Explicitly enable the Safe Browsing function of WebView in the application process -->
    <meta-data
        android:name="android.webkit.WebView.EnableSafeBrowsing"
        android:value="true" />
  </application>
</manifest>
MainActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.webview.safebrowsing;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebView;

import java.util.ArrayList;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    private boolean mSafeBrowsingIsInitialized;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button1).setOnClickListener(setWhiteList);
        findViewById(R.id.button2).setOnClickListener(reload);

        final WebView webView = findViewById(R.id.webView);
        webView.setWebViewClient(new MyWebViewClient());

        mSafeBrowsingIsInitialized = false;
        // Because Safe Browsing is not supported on a device below Android 8.1,
        // real implementation needs to check Android OS version of the device
        WebView.startSafeBrowsing(this, new ValueCallback<Boolean>() {
            @Override
            public void onReceiveValue(Boolean result) {
                mSafeBrowsingIsInitialized = true;
                if (result) {
                    Log.i("WebView SafeBrowsing", "Initialized SafeBrowsing!");
                    webView.loadUrl("http://testsafebrowsing.appspot.com/s/malware.html");
                } else {
                    Log.w("WebView SafeBrowsing", "SafeBrowsing initialization failed...");
                    // When the initilization failed, Safe Browsing might not work
                    // properly. In this case, it is advaisable not to load URL.
                }
            }
        });
    }

    View.OnClickListener setWhiteList = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // Set the white list of the pair of host name and Ip address which
            // is excluded from Safe Browsing
            WebView.setSafeBrowsingWhitelist(new ArrayList<>(Arrays.asList("testsafebrowsing.appspot.com")), new ValueCallback<Boolean>() {
                @Override
                public void onReceiveValue(Boolean aBoolean) {
                    Log.i("WebView SafeBrowsing", "Whitelisted " + aBoolean.toString());
                }
            });
        }
    };

    View.OnClickListener reload = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            final WebView webView = findViewById(R.id.webView);
            webView.reload();
        }
    };
}
MyWebViewClient.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.webview.safebrowsing;

import android.webkit.SafeBrowsingResponse;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MyWebViewClient extends WebViewClient {

    // When Safe Browsing is enabled, accessing unsafe Web page will cause this
    // callback to be invoked
    @Override
    public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
                                  int threatType, SafeBrowsingResponse callback) {
        // Do not display warningpage, and return back to safe page
        callback.backToSafety(true);
        Toast.makeText(view.getContext(), "Because the visiting web page is suspicious to be a malware site, we are returning back to the safe page.", Toast.LENGTH_LONG).show();
    }
}

4.10. Using Notifications

Android offers the Notification feature for sending messages to end users. Using a Notification causes a region known as a status bar to appear on the screen, inside which you may display icons and messages.

_images/image54.png

Fig. 4.10.1 An example of a Notification

The communication functionality of Notifications is enhanced in Android 5.0 (API Level 21) to allow messages to be displayed via Notifications even when the screen is locked, depending on user and application settings. However, incorrect use of Notifications runs the risk that private information—which should only be shown to the terminal user herself—may be seen by third parties. For this reason, this functionality must be implemented with careful attention paid to privacy and security.

The possible values for the Visibility option and the corresponding behavior of Notifications is summarized in the following table.

Table 4.10.1 Possible visibility values and behavior of Notifications
Visibility value Behavior of Notivications
Public Notifications are displayed on all locked screens.
Private Notifications are displayed on all locked screens; however, on locked screens that have been password-protected (secure locks), fields such as the title and text of the Notification are hidden (replaced by publicly-releasable messages in which private information is hidden).
Secret Notifications are not displayed on locked screens that are protected by passwords or other security measures (secure locks). (Notifications are displayed on locked screens that do not involve secure locks.)

4.10.1. Sample Code

When a Notification contains private information regarding the terminal user, a message from which the private information has been excluded must be prepared and added to be displayed in the event of a locked screen.

_images/image55.png

Fig. 4.10.2 A notification on a locked screen

Sample code illustrating the proper use of Notifications for messages containing private data is shown below.

Points:

  1. When using Notifications for messages containing private data, prepare a version of the Notification that is suitable for public display (to be displayed when the screen is locked).
  2. Do not include private information in Notifications prepared for public display (displayed when the screen is locked).
  3. Explicitly set Visibility to Private when creating Notifications.
  4. When Visibility is set to Private, Notifications may contain private information.
VisibilityPrivateNotificationActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.notification.visibilityPrivate;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

public class VisibilityPrivateNotificationActivity extends Activity {
    /**
     * Display a private Notification
     */
    private final int mNotificationId = 0;

    public static final String DEFAULT_CHANNEL = "default_channel";
    public static final String SENDER_NAME = "Sender Name";

    public static final String REMOTE_REPLY = "remote_reply";
    public static final String reply_choices[] = {"choice1", "choice2", "choice3"};
    public static final String REPLY_LABEL = "input reply";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (Build.VERSION.SDK_INT >= 26) {
            // Channel is required for Notification from api level 26	
            NotificationChannel default_channel =
                new NotificationChannel(DEFAULT_CHANNEL,
                    getString(R.string.notification_channel_default),
                    NotificationManager.IMPORTANCE_DEFAULT);

            NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.createNotificationChannel(default_channel);
        }
    }

    public void onSendNotificationClick(View view) {

        // *** POINT 1 *** When preparing a Notification that includes private
        // information, prepare an additional Noficiation for public display
        // (displayed when the screen is locked).
        Notification.Builder publicNotificationBuilder;
        if (Build.VERSION.SDK_INT >= 26) {
            publicNotificationBuilder =
                new Notification.Builder(this, DEFAULT_CHANNEL)
                        .setContentTitle("Notification : Public");
        } else {
            publicNotificationBuilder =
                new Notification.Builder(this)
                        .setContentTitle("Notification : Public");
        }

        if (Build.VERSION.SDK_INT >= 21)
            publicNotificationBuilder
                .setVisibility(Notification.VISIBILITY_PUBLIC);
        // *** POINT 2 *** Do not include private information in Notifications
        // prepared for public display (displayed when the screen is locked).
        publicNotificationBuilder.setContentText("Visibility Public : Omitting sensitive data.");
        publicNotificationBuilder.setSmallIcon(R.drawable.ic_launcher);
        Notification publicNotification = publicNotificationBuilder.build();

        // Construct a Notification that includes private information.
        Notification.Builder privateNotificationBuilder;
        if (Build.VERSION.SDK_INT >= 26) {
            privateNotificationBuilder =
                    new Notification.Builder(this, DEFAULT_CHANNEL)
                            .setContentTitle("Notification : Private");
        } else {
            privateNotificationBuilder =
                    new Notification.Builder(this)
                            .setContentTitle("Notification : Private");
        }

        // *** POINT 3 *** Explicitly set Visibility to Private when creating
        // Notifications.
        if (Build.VERSION.SDK_INT >= 21)
            privateNotificationBuilder
                .setVisibility(Notification.VISIBILITY_PRIVATE);
        // *** POINT 4 *** When Visibility is set to Private, Notifications may
        // contain private information.
        privateNotificationBuilder
            .setContentText("Visibility Private : Including user info.");
        privateNotificationBuilder.setSmallIcon(R.drawable.ic_launcher);
        // When creating a Notification with Visibility=Private, we also create
        // and register a separate Notification with Visibility=Public for public
        // display.
        if (Build.VERSION.SDK_INT >= 21)
            privateNotificationBuilder
                .setPublicVersion(publicNotification);

        if (Build.VERSION.SDK_INT >= 28) {
            // Display resource sample_picture.png in Notification by
            // Notification.MessagingStyle.Message.setData()
            int resourceId = R.drawable.sample_picture_small;
            Uri imageUri = Uri.parse("android.resource://" +
                                     getApplicationContext().getPackageName() +
                                     "/" + resourceId);

            Person sender = new Person.Builder()
                    .setName(SENDER_NAME)
                    .setIcon(null)
                    .build();

            // Prepare Notification.MessagingStyle.Message and set the image with
            // setData()
            Notification.MessagingStyle.Message message =
                new Notification.MessagingStyle
                        .Message("Sample Picture", 0, sender)
                        .setData("image/png", imageUri);

            // Prepare Notification.MessagingStyle and set
            // Notification.MessagingStyle.Message that sets the image
            Notification.MessagingStyle message_style =
                    new Notification.MessagingStyle(sender)
                    .addMessage(message);

            // Set Notification.MessagingStyle to Notification
            privateNotificationBuilder.setStyle(message_style);
        }

        if (Build.VERSION.SDK_INT >= 28) {
            // Display reply options in Notification by
            // RemoteInput.Builder.setChoices()
            Intent intent =
                new Intent( getApplicationContext(), NotificationReceiver.class);

            PendingIntent pendingIntent =
                PendingIntent.getBroadcast(this, 0, intent, 0);

            RemoteInput remoteInput = new RemoteInput.Builder(REMOTE_REPLY)
                    .setLabel(REPLY_LABEL)
                    .setChoices(reply_choices)
                    .build();

            Icon icon = Icon.createWithResource(this, R.drawable.ic_launcher);

            Notification.Action actionReply =
                new Notification.Action.Builder(icon, REPLY_LABEL, pendingIntent)
                    .addRemoteInput(remoteInput)
                    .setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY)
                    .build();

            privateNotificationBuilder.addAction(actionReply);
        }

        Notification privateNotification = privateNotificationBuilder.build();
        //Although not implemented in this sample code, in many cases
        //Notifications will use setContentIntent(PendingIntent intent)
        //to ensure that an Intent is transmission when Notification
        //is clicked. In this case, it is necessary to take steps--depending
        //on the type of component being called--to ensure that the Intent
        //in question is called by safe methods (for example, by explicitly
        //using Intent). For information on safe methods for calling various
        //types of component, see the following sections.
        //4.1. Creating and using Activities
        //4.2. Sending and receiving Broadcasts
        //4.4. Creating and using Services

        NotificationManager notificationManager =
                (NotificationManager) this
                .getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(mNotificationId, privateNotification);
    }
}

4.10.2. Rule Book

When creating Notification, the following rules must be observed.

  1. Regardless of the Visibility setting, Notifications must not contain sensitive information (although private information is an exception) (Required)
  2. Notifications with Visibility=Public must not contain private information (Required)
  3. For Notifications that contain private information, Visibility must be explicitly set to Private or Secret (Required)
  4. When using Notifications with Visibility=Private, create an additional Notification with Visibility=Public for public display (Recommended)

4.10.2.1. Regardless of the Visibility setting, Notifications must not contain sensitive information (although private information is an exception) (Required)

On terminals using Android4.3 (API Level 18) or later, users can use the Settings window to grant apps permission to read Notifications. Apps granted this permission will be able to read all information in Notifications; for this reason, sensitive information must not be included in Notifications. (However, private information may be included in Notifications depending on the Visibility setting).

Information contained in Notifications may generally not be read by apps other than the app that sent the Notification. However, users may explicitly grant permission to certain user-selected apps to read all information in Notifications. Because only apps that have been granted user permission may read information in Notifications, there is nothing problematic about including private information on the user within the Notification. On the other hand, if sensitive information other than the user’s private information (for example, secret information known only to the app developers) is include in a Notification, the user herself may attempt to read the information contained in the Notification and may grant applications permission to view this information as well; thus the inclusion of sensitive information other than private user information is problematic.

For specific methods and conditions, see Section “4.10.3.1. On User-granted Permission to View Notifications

4.10.2.2. Notifications with Visibility=Public must not contain private information (Required)

When sending Notifications with Visibility=Public, private user information must not be included in the Notification. When a Notifications has the setting Visibility=Public, the information in the Notification is displayed even when the screen is locked. This is because such Notifications carry the risk that private information might be seen and stolen by a third party in physical proximity to the terminal.

VisibilityPrivateNotificationActivity.java
    // Prepare a Notification for public display (to be displayed on locked
    // screens) that does not contain sensitive information.
    Notification.Builder publicNotificationBuilder =
        new Notification.Builder(this).setContentTitle("Notification : Public");

    publicNotificationBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
    // Do not include private information in Notifications for public display
    // (to be displayed on locked screens).
    publicNotificationBuilder.setContentText("Visibility Public: sending notification without sensitive information.");
    publicNotificationBuilder.setSmallIcon(R.drawable.ic_launcher);

Typical examples of private information include emails sent to the user, the user’s location data, and other items listed in Section “5.5. Handling privacy data”.

4.10.2.3. For Notifications that contain private information, Visibility must be explicitly set to Private or Secret (Required)

Terminals using Android 5.0 (API Level 21) or later will display Notifications even when the screen is locked. Thus, when the Notification contains private information, its Visibility flag should be set explicitly to “Private” or “Secret”. This is to protect against the risk of private information contained in a Notification being displayed on a locked screen.

At present, the default value of Visibility is set to Private for Notifications, so the aforementioned risk will only arise if this flag is explicitly changed to Public. However, the default value of Visibility may change in the future; for this reason, and also for the purpose of clearly communicating one’s intentions at all times when handling information, it is mandatory to set Visibility=Private explicitly for Notifications that contain private information.

VisibilityPrivateNotificationActivity.java
    // Create a Notification that includes private information.
    Notification.Builder priavteNotificationBuilder =
        new Notification.Builder(this).setContentTitle("Notification : Private");

    // *** POINT *** Explicitly set Visibility=Private when creating the
    // Notification.
    priavteNotificationBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);

4.10.2.4. When using Notifications with Visibility=Private, create an additional Notification with Visibility=Public for public display (Recommended)

When communicating information via a Notification with Visibility=Private, it is desirable to create simultaneously an additional Notification, for public display, with Visibility=Public; this is to restrict the information displayed on locked screens.

If a public-display Notification is not registered together with a Visibility=Private notification, a default message prepared by the operating system will be displayed when the screen is locked. Thus there is no security problem in this case. However, for the purpose of clearly communicating one’s intentions at all times when handling information, it is recommended that a public-display Notification be explicitly created and registered.

VisibilityPrivateNotificationActivity.java
    // Create a Notification that contains private information.
    Notification.Builder privateNotificationBuilder =
        new Notification.Builder(this).setContentTitle("Notification : Private");

    // *** POINT *** Explicitly set Visibility=Private when creating the
    // Notification.
    if (Build.VERSION.SDK_INT >= 21)
        privateNotificationBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
    // *** POINT *** Notifications with Visibility=Private may include private
    // information.
    privateNotificationBuilder
        .setContentText("Visibility Private : Including user info.");
    privateNotificationBuilder.setSmallIcon(R.drawable.ic_launcher);
    // When creating a Notification with Visibility=Private,
    // simultaneously create and register a public-display Notification with
    // Visibility=Public.
    if (Build.VERSION.SDK_INT >= 21)
        privateNotificationBuilder.setPublicVersion(publicNotification);

4.10.3. Advanced Topics

4.10.3.1. On User-granted Permission to View Notifications

As noted above in Section “4.10.2.1. Regardless of the Visibility setting, Notifications must not contain sensitive information (although private information is an exception) (Required)”, on terminals using Android 4.3 (API Level 18) or later, certain user-selected apps that have been granted user permission may read information in all Notifications.

_images/image56.png

Fig. 4.10.3 “The Access to Notifications” window, from which Notification read controls may be configured

The following sample code illustrates the use of NotificationListenerService.

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.notification.notificationListenerService">

  <application
      android:allowBackup="false"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    <service android:name=".MyNotificationListenerService"
             android:label="@string/app_name"
             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
      <intent-filter>
        <action android:name=
                "android.service.notification.NotificationListenerService" />
      </intent-filter>
    </service>
  </application>
</manifest>
MyNotificationListenerService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.notification.notificationListenerService;

import android.app.Notification;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;

public class MyNotificationListenerService extends NotificationListenerService {
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        // Notification is posted.
        outputNotificationData(sbn, "Notification Posted : ");
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        // Notification is deleted.
        outputNotificationData(sbn, "Notification Deleted : ");
    }

    private void outputNotificationData(StatusBarNotification sbn, String prefix) {
        Notification notification = sbn.getNotification();
        int notificationID = sbn.getId();
        String packageName = sbn.getPackageName();
        long PostTime = sbn.getPostTime();

        String message = prefix + "Visibility :" + notification.visibility +
                         " ID : " + notificationID;
        message += " Package : " + packageName + " PostTime : " + PostTime;

        Log.d("NotificationListen", message);
    }
}

As discussed above, by using NotificationListenerService to obtain user permission it is possible to read Notifications. However, because the information contained in Notifications frequently includes private information on the terminal, care is required in handling such information.

4.10.3.2. Touch to Be Passed Through Specific Window

On Android 12 models and later, if the application is displaying an unsafe overlay, touches that pass through specific windows are blocked. This change of operation affects all applications running in Android 12 regardless of targetSdkVersion.

Examples of unsafe overlay displays are as follows.

  • Overlay displays that require the SYSTEM_ALERT_WINDOW permission, such as windows that use the TYPE_APPLICATION_OVERLAY layer and the FLAG_NOT_TOUCHABLE flag
  • Activity windows which use the FLAG_NOT_TOUCHABLE flag

However, in the following cases, touches that pass through are still available.

  • If displaying overlays only within the application
  • For trusted windows with TYPE_ACCESSIBILITY_OVERLAY, TYPE_INPUT_METHOD, etc. specified to the layers
  • For invisible windows with route view set to GONE or INVISIBLE
  • For windows with 0 set as the alpha value
  • For windows with TYPE_APPLICATION_OVERLAY specified to the layer that has an alpha value lower than the specified value [38].
[38]Specified value indicates a value that can be acquired by InputManager.getMaximumObscuringOpacityForTouch(). If numerous windows are overlapping, the total alpha value must be lower than the specified value.

If an unsafe overlay is displayed on the application, it is recommended to use one of the following APIs based on the use case.

  • Bubble

Bubble is a function that was added from Android 11 to make Notification use easier. The message notification appears as a Bubble on other applications when you have received messages, enabling you to display and reply to the message without having to switch to the application that received the message.

  • Picture-in-Picture (PIP)

PIP is a function that displays content on a small window that is fixed to the corner of the screen even while you are moving around various applications or browsing through content on the main screen. Available on Android 8.0 and later.

  • Notification

Notification is a standard method to provide reminders to users, messages from other people, and timely information from applications while reducing device usage to the minimum. Users can open the application by tapping the notification or run direct action from the notification.

  • Snackbar

Snackbar is a notification function that displays messages for a short period of time while the application is running.

  • Toast

A notification function that displays messages for a short period of time as on Snackbar. Use Toast if message display is required while the application is in the background. To grant permission to untrusted touches, run the following adb command on the terminal window.

    # A specific app
    adb shell am compat disable BLOCK_UNTRUSTED_TOUCHES com.example.app
    
    # All apps
    # If you'd still like to see a Logcat message warning when a touch would be
    # blocked, use 1 instead of 0.
    adb shell settings put global block_untrusted_touches 0

To return the operation to the default operation that blocks untrusted touches, run the following adb command on the terminal window.

    # A specific app
    adb shell am compat reset BLOCK_UNTRUSTED_TOUCHES com.example.app
    
    # All apps
    adb shell settings put global block_untrusted_touches 2

4.10.3.3. Mutability of the PendingIntent Object

The application that targets Android 12 requires specification for the mutability of the PendingIntent object. Mutability is specified using the PendingIntent.FLAG_MUTABLE flag for mutable and PendingIntent.FLAG_IMMUTABLE flag for immutable.

If attempting to create a PendingIntent object without specifying any flag, an IllegalArgumentException occurs, and the following message is displayed on LogCat.

Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.

It is recommended to specify the FLAG_IMMUTABLE flag in terms of security enhancement. The specification is performed as follows.

PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);

However, as with 4.10.1.Sample Code, if the direct reply action in the notification requires a change to the clip data in the PendingIntent object that is associated with the reply, FLAG_MUTABLE must be specified.

4.11. Using Shared Memory

Previously, the Android OS included a shared memory mechanism, and it was provided by android.os.MemoryFile. However, it did not directly provide APIs or access control for sharing over multiple applications, and it was difficult to use for general applications. In Android 8.1 (API level 27), the android.os.SharedMemory package was introduced, which enabled the shared memory mechanism to be used relatively easily from general applications. At the time of Android 8.1, MemoryFile is a wrapper of SharedMemory, and use of SharedMemory is recommended. This section describes the important security points when using this SharedMemory API.

As described later, this API was built assuming a structure where a provided application and memory are shared when a service of an application creates a shared memory and provides this shared memory to other applications. And so, all the information described in “4.4. Creating/Using Services” also applies to applications that provide shared memory and applications that use this shared memory. If you have not already read this information, it is recommended that you read “4.4. Creating/Using Services” before proceeding to the explanation below.

No Android Support Library is available that supports the SharedMemory API. For this reason, to operate applications using SharedMemory in systems that are below API level 27, measures are required such as by implementing an equivalent virtual memory mechanism, such as by wrapping C language level APIs using JNI, and the processes must be separated based on the version.

4.11.1. Overview of Android Shared Memory

Shared memory is a mechanism for sharing the same physical memory area among multiple applications.

_images/image96.png

Fig. 4.11.1 Overview of Shared Memory

The figure above shows the appearance when using a shared memory for application A and application B. Application A creates a shared memory object, and it is provided to application B. The role of providing shared memory by application A is handled as a service of application A. Application B connects to this service, requests and obtains the shared memory, and after the processes required by the shared memory are completed, application B notifies application A that use is completed.

For example, if handling data where the maximum size (1 MB [39]) for allowable communication between normal processes is exceeded, such as bitmap data of a large image, shared memory can be used to enable sharing among multiple processes. Also, the amount of memory used for the entire device can be reduced for enabling normal memory access, and this allows for extremely high-speed communication between processes. However, because multiple applications are simultaneously accessing in parallel, consideration must also be made for maintaining the integrity of the data in certain cases. To avoid this, exclusive control can be performed between applications, and other careful designs are needed to ensure that the memory area is properly divided and the accessed areas do not interfere with each other.

[39]https://developer.android.com/guide/components/activities/parcelables-and-bundles

As mentioned above, the shared memory API of Android SDK was built so that a service creates a shared memory object and provides it to other processes. Because the shared memory class (android.os.SharedMemory) is defined as parcelable, the shared memory instance can be easily passed on to other processes through binders. An overview of the exchanges between the service and client in the sample code appearing later has the structure shown in the figure below (this can vary significantly depending on the structure of the service).

_images/image97.png

Fig. 4.11.2 Exchanges between shared memory service and client

  • S1. A service uses SharedMemory.create() to create a shared memory.
  • S2. If the service itself will use the shared memory, SharedMemory#map() is used to map the shared memory to its own memory space.
  • C1. The client uses explicit intent to connect to the service by Context#bindService().
  • S3. When a connection request is received from the client, the service’s onBind() call back is called. The service performs the required pre-processing (if needed) at this stage and returns a IBinder for connection to the client.
  • C2. The return value (IBinder instance) when the service executed onBind() is returned as an argument of onServiceConnected() callback on the client side. Then, this IBinder is used to perform communication with the service.
  • C3. The client requests the shared memory for the service.
  • S4. The service receives a shared memory request from the client and sets the operations permitted (read, write) when the client accesses the shared memory.
  • S5. The service passes on the shared memory object to the client.
  • C4. To access the received shared memory, the client maps the shared memory to its own address space for use.
  • C5, C6. When the client has finished use of the shared memory, the shared memory is unmapped (C5) from its own memory space, and the shared memory is closed (C6).
  • C7. Then, the client notifies the server that use of the shared memory is completed.
  • C8. The client disconnects from the service.
  • S7. After the message that usage is completed is received from the client, the service itself also unmaps and closes the shared memory.

The onServiceConnected() in item C2 above is defined as a class where the android.content.ServiceConnection class is implemented. For specific examples, refer to the sample code appearing later. Several communication methods using IBinder are available, but Messenger is used in the sample code.

4.11.2. Sample Code

As described before, the side that creates the shared memory and provides it to other applications is implemented as a service. For this reason, from the standpoint of security for functions and information sharing, there are no fundamental differences from the information contained in “4.4. Creating/Using Services” Based on the classifications in 4.4., the figure below shows the process for determining who the memory will be shared with.

_images/image98.png

Fig. 4.11.3 Flow Figure to select SharedMemory Service Type

Table 4.4.2 in “4.4.1. Sample Code” describes how a service is implemented, but for shared memory, sharing with other applications must be implemented using a binder. And so, shared memory cannot be implemented as a startService or IntentService service. For this reason, it is implemented as shown in the table below.

Table 4.11.1 Service Category and Types(Shared Memory)
Category Private Service Public Service Partner Service In-house Service
startService type - - - -
IntentService type - - - -
local bind type OK - - -
Messenger bind type OK OK - OK*
AIDL bind type OK OK OK OK

The overall structure is virtually identical to that in “4.4.1. Sample Code” Also, because the items specific to shared memory are the same in all cases, in the specific sample code, the items marked with an asterisk in the above table indicate those that apply to in-house services only. For this reason, to use shared memory in other cases, refer to the information from “4.4.1.1. Creating/Using Private Services” to “4.4.1.3. Creating/Using Partner Services”.

4.11.2.1. Creating/Using Private Services

In this case, a structure is used that shares shared memory created by a private service between multiple processes contained in the application. Also, this private service is started as a process independent from the main process of the application.

Points:

  1. The service that creates the shared memory is explicitly set to private by exported=”false”.
  2. If a process in an application references data that was written by another process, the safety is verified even if it is a process within the same application.
  3. Sensitive information can be shared because the sharing of memory is a process within the same application.

The sample code in “4.4.1.1. Creating/Using Private Services” used services by Intent, but for shared memory, memory resources cannot be shared through Intent, and so a method based on local bind, Message bind, or AIDL bind must be used.

4.11.2.2. Creating/Using Public Services

As described in “4.4.1.2. Creating/Using Public Services,” a public service is a service which is assumed to be used by an unspecified large number of applications. As a result, use by malware must also be assumed. Generally, attention must be paid to the points mentioned in 4.4.1.2., but those points are rephrased below from the standpoint of shared memory.

Points (Creating a Service):

  1. Explicitly set to public using exported=”true”.
  2. Verify the safety of parameters and data contained in requests and other operations for starting services and sharing memory.
  3. Sensitive information must not be shared using shared memory.

Points (Using a Service):

  1. Sensitive information must not be written to shared memory.
  2. Safety is verified when referencing data that was written by another application.

4.11.2.3. Creating/Using Partner Services

This information is virtually identical to the information shown in “4.4.1.3. Creating/Using Partner Services”, but this is rephrased from the standpoint of shared memory for showing the following points (Like the sample code in 4.4.1.3., this assumes use of the AIDL bind service)

Points (Creating a Service):

  1. Do not define the Intent Filter, and explicitly declare exported=”true”.
  2. Verify the requesting application’s certificate through a predefined whitelist.
  3. onBind(onStartCommand, onHandleIntent) cannot be used to determine whether the requester is a partner.
  4. Verify the safety of received Intent even if the Intent was sent from a partner application.
  5. Writing to the shared memory is permissible only for information that is allowed to be disclosed to the partner application.

Points (Using a Service):

  1. Verify that the certificate of the requesting partner service application is registered in the whitelist.
  2. Writing to the shared memory is permissible only for information that is allowed to be disclosed to the requesting partner application.
  3. Use explicit Intent to call a partner service.
  4. Verify the safety of the data even if the data was written by a partner application.

4.11.2.4. Creating/Using In-house Services

This section presents an example where shared memory is provided by a service available as public, but the shared memory is provided to an in-house application only. Like the example in “4.4.1.4. Creating/Using In-house Services”, a Messenger bind service is used. The principles and settings for the background are described in 4.4.1.4., and so refer to 4.4.1.4. first if you have not already read this information.

Sample code for application at service side (Messenger bind)

Points are shown below, but items 1 to 5 and 7 are presented in “4.4.1.4. Creating/Using In-house Services,” and item 6 is the only item specific to shared memory.

Points:

  1. Define an in-house signature permission.
  2. Request declaration of the in-house signature permission.
  3. Do not define the Intent Filter, and explicitly declare exported=”true”.
  4. Verify that the in-house signature permission is defined by an in-house application.
  5. Verify the safety of received Intent even if the Intent was sent from an in-house application.
  6. Before passing the shared memory on to a client, use SharedMemory#setProtect() to limit the available operations by the client.
  7. Sign the APK using the same developer key as the requesting application.

For purposes of simplification, this example defines the service that allocates the shared memory and the activity that uses the service within the same application (service is started as a separate process within the same application). For this reason, both the signature permission definition and use declaration are contained in the manifest file.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.sharedmemory.inhouseservice.messenger">
  
  <!-- *** POINT 1 *** Define an in-house signature permission -->
  <permission android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION"
              android:protectionLevel="signature" />
  
  <!-- *** POINT 8 *** Define an in-house signature permission -->
  <uses-permission
      android:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" />
  
  <application
      android:allowBackup="false"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">
    <activity android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <!-- Service which utilizes Messenger -->
    <!-- *** POINT 2 *** Request declaration of the in-house signature permission -->
    <!-- *** POINT 3 *** Do not define the Intent Filter, and explicitly declare exported=”true” -->
    <!-- For purposes of simplification, make the service which provide shared memory to be a different process in the same application -->
    <service android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.SHMService"
             android:exported="true"
             android:permission="org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION"
             android:process=".shmService" />
  </application>
  
</manifest>
SHMService.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.sharedmemory.inhouseservice.messenger;

import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.util.Log;
import android.widget.Toast;

import java.nio.ByteBuffer;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.system.OsConstants.PROT_EXEC;
import static android.system.OsConstants.PROT_READ;
import static android.system.OsConstants.PROT_WRITE;

public class SHMService extends Service {
    // In-house Signature Permission
    private static final String MY_PERMISSION =
        "org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION";

    // Hash value of the certificate of In-house applications
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Hash value of the certificate "androiddebugkey" stored in
                // debug.keystore
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Hash value of the certificate "my company key" in keystore
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    private final String TAG = "SHM";

    // Strings which will be sent to client
    private final String greeting = "Hi! I send you my memory. Let's Share it!";
    // Page size is 4K bytes
    public static final int PAGE_SIZE = 1024 * 4;
    // In this example, we use two SharedMemory objects
    // Client side specify the one of these SharedMemory by using following
    // identify
    public static final int SHMEM1 = 0;
    public static final int SHMEM2 = 1;

    // Instances of Shared Memory
    // mSHMem1: used for sending data to client
    private SharedMemory mSHMem1 = null;
    // ByteBuffer for mapping mSHMem
    private ByteBuffer m1Buffer1;
    // mSHMem2: used for receiving data from client side
    private SharedMemory mSHMem2 = null;
    // ByteBuffer for mapping mSHMem2
    private ByteBuffer m2Buffer1;
    private ByteBuffer m2Buffer2;
    // true iff all ByteBuffers are mapped successfully
    private boolean mBufferMapped = false;


    // In this example, Messenger is used for communicating with client
    // The follwings are message identifier for the communication
    public static final int MSG_INVALID = Integer.MIN_VALUE;
    public static final int MSG_ATTACH  =
            MSG_INVALID + 1; // client requests SHMEM1
    public static final int MSG_ATTACH2 =
            MSG_ATTACH + 1;  // client requests SHMEM2
    public static final int MSG_DETACH  =
            MSG_ATTACH2 + 1; // client no more need SHMEM1
    public static final int MSG_DETACH2 =
            MSG_DETACH + 1;  // client no more need SHMEM2
    public static final int MSG_REPLY1  =
            MSG_DETACH2 + 1; // first reply from client
    public static final int MSG_REPLY2  =
            MSG_REPLY1 + 1;  // second reply from client
    public static final int MSG_END     =
            MSG_REPLY2 + 1;  // Service declared the end of the session

    // Handler manipulating Message received from client
    private class CommHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_ATTACH:
                Log.d(TAG, "got MSG_ATTACH");
                shareWith1(msg);
                break;
            case MSG_ATTACH2:
                Log.d(TAG, "got MSG_ATTACH2");
                shareWith2(msg);
                break;
            case MSG_DETACH:
                Log.d(TAG, "got MSG_DETACH");
                unShare(msg);
                break;
            case MSG_REPLY1:
                Log.d(TAG, "got MSG_REPLY1");
                gotReply(msg);
                break;
            case MSG_REPLY2:
                Log.d(TAG, "got MSG_REPLY2");
                gotReply2(msg);
                break;
            default:
                invalidMsg(msg);
            }
        }
    }

    private final Handler mHandler = new CommHandler();

    // Messenger used for receiving data from client
    private final Messenger mMessenger = new Messenger(mHandler);

    // When bound, extract Binfer from Message, pass it to client
    @Override
    public IBinder onBind(Intent intent) {
        // ** POINT 4 *** Verify that the in-house signature permission is defined
        // by an in-house application.
        if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
            Toast.makeText(this, "In-house signature permission is not defined by an in-house application.", Toast.LENGTH_LONG).show();
            return null;
        }

        // *** POINT 5 *** Verify the safety of received Intent even if the Intent
        // was sent from an in-house application
        // Omitted because this is an sample code. Refer to
        // "3.2 Handling Input Data Carefully and Securely".
        String param = intent.getStringExtra("PARAM");
        Log.d(TAG, String.format("Received Parameter [%s]!", param));
        return mMessenger.getBinder();
    }

    // Mapping layout
    // Offset must be page boundary
    public static final int SHMEM1_BUF1_OFFSET = 0;
    public static final int SHMEM1_BUF1_LENGTH = 1024;
    public static final int SHMEM2_BUF1_OFFSET = 0;
    public static final int SHMEM2_BUF1_LENGTH = 1024;
    public static final int SHMEM2_BUF2_OFFSET = PAGE_SIZE;
    public static final int SHMEM2_BUF2_LENGTH = 128;

    // Allocate 2 SharedMemory objects
    private boolean allocateSharedMemory() {
        try {
            // For sending data to client
            mSHMem1 = SharedMemory.create("SHM", PAGE_SIZE);
            // For receiving data from client
            mSHMem2 = SharedMemory.create("SHM2", PAGE_SIZE * 2);
        } catch (ErrnoException e) {
            Log.e(TAG, "failed to allocate shared memory" + e.getMessage());
            return false;
        }
        return true;
    }

    // Map specified SharedMemory
    private ByteBuffer mapShared(SharedMemory mem,
                                 int prot, int offset, int size) {
        ByteBuffer tBuf ;
        try {
            tBuf = mem.map(prot, offset, size);
        } catch (ErrnoException e) {
            Log.e(TAG, "could not map, prot=" + prot + ", offset=" + offset + ", length=" + size + "\n " + e.getMessage() + "err no. = " + e.errno);
            return null;
        } catch (IllegalArgumentException e){
            Log.e(TAG, "map failed: " + e.getMessage());
            return null;
        }
        Log.d(TAG, "mmap success: prot=" + prot);
        return tBuf;
    }

    // Server side mappings of SharedMemory objects
    private void mapMemory() {
        // mSHMem1: read/write
        m1Buffer1 = mapShared(mSHMem1,
            PROT_READ | PROT_WRITE | PROT_EXEC, SHMEM1_BUF1_OFFSET,
            SHMEM1_BUF1_LENGTH);
        // mSHMem2: separate two regions, read/write for each
        m2Buffer1 = mapShared(mSHMem2,
            PROT_READ | PROT_WRITE, SHMEM2_BUF1_OFFSET,
            SHMEM2_BUF1_LENGTH);
        m2Buffer2 = mapShared(mSHMem2,
            PROT_READ | PROT_WRITE, SHMEM2_BUF2_OFFSET,
            SHMEM2_BUF2_LENGTH);

        if (m1Buffer1 != null && m2Buffer1 != null && m2Buffer2 != null) {
            mBufferMapped = true;
        }
    }

    // Free SharedMemory
    private void deAllocateSharedMemory () {
        if (mBufferMapped) {
            if (mSHMem1 != null) {
                if (m1Buffer1 != null) SharedMemory.unmap(m1Buffer1);
                m1Buffer1 = null;
                mSHMem1.close();
                mSHMem1 = null;
            }
            if (mSHMem2 != null) {
                if (m2Buffer1 != null) SharedMemory.unmap(m2Buffer1);
                if (m2Buffer2 != null) SharedMemory.unmap(m2Buffer2);
                m2Buffer1 = null;
                m2Buffer2 = null;
                mSHMem2.close();
                mSHMem2 = null;
            }
            mBufferMapped = false;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // Allocate SharedMemory objects at the time of instantiation
        // If succeded, map SharedMemory objects
        if (allocateSharedMemory()) {
            mapMemory();
        }
    }

    // Provide SHMEM1 to client
    private void shareWith1(Message msg){
        // If failed in allocating or mapping, do nothing
        if (!mBufferMapped) return;

        // *** POINT 6 *** Before passing the shared memory on to a client, use
        // SharedMemory#setProtect() to limit the available operations by the
        // client.
        // Client can only read from mSHMem1
        mSHMem1.setProtect(PROT_READ);

        // Mapping hash been done before the above setProtect(PROT_READ),
        // so setver side can write to mShMem1 via m1Buffer1
        // Put the size of the messege, then add message string
        m1Buffer1.putInt(greeting.length());
        m1Buffer1.put(greeting.getBytes());

        try {
            // Pass the SharedMemory object to the client
            Message sMsg = Message.obtain(null, SHMEM1, mSHMem1);
            msg.replyTo.send(sMsg);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to share" + e.getMessage());
        }
    }

    // Provide SHMEM2
    private void shareWith2(Message msg) {
        if (!mBufferMapped) return;

        // *** POINT 6 *** Before passing the shared memory on to a client, use
        // SharedMemory#setProtect() to limit the available operations by the
        // client.
        // Client can write to mSHMem2
        mSHMem2.setProtect(PROT_WRITE);
        // Set messages to client in each buffer
        final String greeting2 = "You can write here!";
        m2Buffer1.putInt(greeting2.length());
        m2Buffer1.put(greeting2.getBytes());
        final String greeting3 = "From this point, I'll also write.";
        m2Buffer2.putInt(greeting3.length());
        m2Buffer2.put(greeting3.getBytes());
        try {
            // Pass the shared memory objects to the client
            Message sMsg = Message.obtain(null, SHMEM2, mSHMem2);
            msg.replyTo.send(sMsg);
        } catch (RemoteException e){
            Log.e(TAG, "failed to share mSHMem2" + e.getMessage());
        }
    }

    // Stop sharing memory
    private void unShare(Message msg){
        deAllocateSharedMemory();
    }

    // Accepted invalid message
    private void invalidMsg(Message msg){
        Log.e(TAG, "Got an Invalid message: " + msg.what);
    }

    // Retrive data which set by the client from buffer
    // The first element is a size of the data followed by byte sequence of a
    // string
    private String extractReply (ByteBuffer buf){
        int len = buf.getInt();
        byte [] bytes = new byte[len];
        buf.get(bytes);
        return new String(bytes);
    }

    // In this example, server side accepts two types of message from the client
    // goReply() assumes that  m1Buffer1 holds a data from the client
    private void gotReply(Message msg) {
        m1Buffer1.rewind();
        String message = extractReply(m1Buffer1);
        if (!message.equals(greeting)){
            Log.e(TAG, "my message was overwritten: " + message);
        }
    }
    // got Reply2() assumes m2Buffer1 holds a data from the client
    private void gotReply2(Message msg) {
        m2Buffer1.rewind();
        String message = extractReply(m2Buffer1);
        android.util.Log.d(TAG, "got a message of length " + message.length() +
                                 " from client: " + message);
        // Accepting a message in m2Buffer1 is a sign of the end of sharing memory
        Message eMsg = Message.obtain();
        eMsg.what = MSG_END;
        try {
            msg.replyTo.send(eMsg);
        } catch (RemoteException e){
            Log.e(TAG, "error in reply 2: " + e.getMessage());
        }
    }
}
SigPerm.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName,
                               String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try {
            // Get the package name of the application which declares a permission
            // named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi =
                pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            // Fail if the permission named sigPermName is not a Signature
            // Permission
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE)
                return false;

            // Compare the actual hash value of pkgname with the correct hash
            // value.
            if (Build.VERSION.SDK_INT >= 28) {
                // ** if API Level >= 28, direct check is possible
                return pm.hasSigningCertificate(pkgname,
                                                Utils.hex2Bytes(correctHash),
                                                CERT_INPUT_SHA256);
            } else {
                // else(API Level < 28) use the facility of PkgCert
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
                        
        } catch (NameNotFoundException e) {
            return false;
        }
    }
}
PkgCert.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {

    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo =
                pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            // Will not handle multiple signatures.
            if (pkginfo.signatures.length != 1) return null;
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

*** Point 7 *** When exporting an APK, sign the APK with the same developer key as the requesting application.

_images/image35.png

Fig. 4.11.4 Signing the APK with the same developer key as the requesting application

Sample code for client

Points:

  1. Declare use of the in-house signature permission.
  2. Verify that the in-house-defined signature permission is defined by the in-house application.
  3. Verify that the destination application is signed by the in-house certificate.
  4. Sensitive information can be sent because the destination application is in-house.
  5. Use explicit Intent to call an in-house service.
  6. Sign the APK using the same developer key as the destination application.

All the points shown here are the same as the points for the client in “4.4.1.4. Creating/Using In-house Services”, and no points are specific to shared memory. Basic points on using shared memory are shown in the sample code below, and so refer to it for further information.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.sharedmemory.inhouseservice.messenger">
  
  <!-- *** POINT 1 *** Define an in-house signature permission -->
  <permission android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION"
              android:protectionLevel="signature" />
  
  <!-- *** POINT 8 *** Define an in-house signature permission -->
  <uses-permission
      android:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" />
  
  <application
      android:allowBackup="false"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">
    <activity android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <!-- Service which utilizes Messenger -->
    <!-- *** POINT 2 *** Request declaration of the in-house signature permission -->
    <!-- *** POINT 3 *** Do not define the Intent Filter, and explicitly declare exported=”true” -->
    <!-- For purposes of simplification, make the service which provide shared memory to be a different process in the same application -->
    <service android:name="org.jssec.android.sharedmemory.inhouseservice.messenger.SHMService"
             android:exported="true"
             android:permission="org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION"
             android:process=".shmService" />
  </application>
  
</manifest>
MainActivity.java
/*
 * Copyright (C) 2012-2021 Japan Smartphone Security Association
 *
 * 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
 *
 *      http://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 org.jssec.android.sharedmemory.inhouseservice.messenger;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.widget.Toast;
import android.util.Log;

import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;

import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;

import static android.system.OsConstants.PROT_EXEC;
import static android.system.OsConstants.PROT_READ;
import static android.system.OsConstants.PROT_WRITE;

public class MainActivity extends Activity {

    private final String TAG = "SHMClient";

    // Messenger used for sending data to Service
    private Messenger mServiceMessenger = null;

    // SharedMemory objects
    private SharedMemory myShared1;
    private SharedMemory myShared2;
    // ByteBuffers for mapping SharedMemories
    private ByteBuffer mBuf1;
    private ByteBuffer mBuf2;

    // Information of using Activity
    private static final String SHM_PACKAGE =
        "org.jssec.android.sharedmemory.inhouseservice.messenger";
    private static final String SHM_CLASS =
        "org.jssec.android.sharedmemory.inhouseservice.messenger.SHMService";

    // In-house Signature Permission
    private static final String MY_PERMISSION =
        "org.jssec.android.sharedmemory.inhouseservice.messenger.MY_PERMISSION";

    // Hash value of the certification of In-house applications
    private static String sMyCertHash = null;
    private static String myCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // Hash value of the certificate "androiddebugkey" stored in
                // debug.keystore
                sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
            } else {
                // Hash value of the certificate "my company key" in keystore
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    // true iff connecting to Service
    private boolean mIsBound = false;

    // Handler handling Messages received from Server
    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHMService.SHMEM1:
                // SHMEM 1 is provided from Service
                // ShareMemory object is stored in Message.obj
                Log.d(TAG, "got SHMEM1");
                myShared1 = (SharedMemory) msg.obj;
                useSHMEM1();
                break;
            case SHMService.SHMEM2:
                // SHMEM2 is provided from Service
                Log.d(TAG, "got SHMEM2");
                myShared2 = (SharedMemory) msg.obj;
                useSHMEM2();
                break;
            case SHMService.MSG_END:
                Log.d(TAG, "got MSG_END");
                alloverNow();
                break;
            default:
                Log.e(TAG, "invalid message: " + msg.what);
            }
        }
    }

    private Handler mHandler = new MyHandler();

    // Messanger used when receiving data from Service
    private Messenger mLocalMessenger = new Messenger(mHandler);

    // Connection used for connecting to Service
    // This is needed if implementation uses bindService
    private class MyServiceConnection implements ServiceConnection {
        // called when connected with Service
        public void onServiceConnected(ComponentName className, IBinder service){
            mServiceMessenger = new Messenger(service);
            // When bound to SharedMemory Service, request 1st SharedMemory
            sendMessageToService(SHMService.MSG_ATTACH);
        }
        // This is called when Service unexpectedly terminate and connection is
        // broken
        public void onServiceDisconnected(ComponentName className){
            mIsBound = false;
            mServiceMessenger = null;
        }
    }
    private MyServiceConnection mServiceConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        doBindService ();
    }

    // Connect to Shared Memory Service
    private void doBindService () {
        mServiceConnection = new MyServiceConnection();
        if (!mIsBound) {
            // *** POINT 9 *** Verify that the in-house-defined signature
            // permission is defined by the in-house application.
            if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
                Toast.makeText(this,  "In-house signature permission is not defined by an in-house application.", Toast.LENGTH_LONG).show();
                return;
            }
            // *** POINT 10 *** Verify that the destination application is signed
            // by the in-house certificate.
            if (!PkgCert.test(this, SHM_PACKAGE, myCertHash(this))) {
                Toast.makeText(this, "Binding Service is not an in-house application.", Toast.LENGTH_LONG).show();
                return;
            }
        }
        Intent it = new Intent();
        // *** POINT 11 *** Sensitive information can be sent because the
        // destination application is in-house.
        it.putExtra("PARAM", "Sensitive Information");

        // *** POINT 12 *** Use explicit Intent to call an in-house service
        it.setClassName(SHM_PACKAGE, SHM_CLASS);

        if (!bindService(it, mServiceConnection, Context.BIND_AUTO_CREATE)) {
            Toast.makeText(this, "Bind Service Failed", Toast.LENGTH_LONG).show();
            return;
        }
        mIsBound = true;
    }

    // Unbind connection with Service
    private void releaseService () {
        unbindService(mServiceConnection);
    }

    // An example of using SHMEM1
    private void useSHMEM1 () {
        // Because only read access is permitted for SHMEM1, mapping with
        // different protection mode will raise an exception.
        // The exception will be handled by mapMemory()
        mBuf1 = mapMemory(myShared1, PROT_WRITE, SHMService.SHMEM1_BUF1_OFFSET,
                          SHMService.SHMEM1_BUF1_LENGTH);
        // map with PROT_READ
        mBuf1 = mapMemory(myShared1, PROT_READ, SHMService.SHMEM1_BUF1_OFFSET,
                          SHMService.SHMEM1_BUF1_LENGTH);
        // Read data which Service side set
        int len = mBuf1.getInt();
        byte[] bytes = new byte[len];
        mBuf1.get(bytes);
        String message = new String(bytes);
        Toast.makeText(MainActivity.this,
                       "Got: " + message, Toast.LENGTH_LONG).show();
        // Because the buffer is read only, writing will cause
        // ReadOnlyBufferException
        try {
            mBuf1.putInt(0);
        } catch (ReadOnlyBufferException e){
            Log.e(TAG, "Write to read only buffer: " + e.getMessage());
        }
        // Reply to Service
        sendMessageToService(SHMService.MSG_REPLY1);
        // then, request a SharedMemory with write permission
        sendMessageToService(SHMService.MSG_ATTACH2);
    }

    // An example of using SHMEM2
    private void useSHMEM2 () {
        // We are allowed to write into SHMEM2, map it with PROT_WRITE
        // Service side set SHMEM2 as PROT_WRITE, so mapping with
        // PROT_READ | PROT_WRITE will raise an exception
        mBuf2 = mapMemory(myShared2, PROT_WRITE, SHMService.SHMEM2_BUF1_OFFSET,
                          SHMService.SHMEM2_BUF1_LENGTH);
        if (mBuf2 != null) {
            // Even if the protection mode is PROT_WRITE only, it will also be
            // readable on most SoC.
            int size = mBuf2.getInt();
            byte [] bytes = new byte[size];
            mBuf2.get(bytes);
            String msg = new String(bytes);
            Log.d(TAG, "Got a message from service: " + msg);

            // Accessing outside of the mapped region will cause
            // IndexOutOfBoundsException
            try {
                mBuf2.get(SHMService.SHMEM2_BUF1_LENGTH + 1);
            } catch (IndexOutOfBoundsException e){
                Log.e(TAG, "out of bound: " + e.getMessage());
            }

            // Override the data which Service side set before
            String replyStr = "OK Thanks!";
            mBuf2.putInt(replyStr.length());
            mBuf2.put(replyStr.getBytes());
            // Reply to Service
            sendMessageToService(SHMService.MSG_REPLY2);
        }
    }

    // Map specified SharedMemory
    private ByteBuffer mapMemory(SharedMemory mem, int proto, int offset,
                                 int length){
        ByteBuffer tempBuf;
        try {
            tempBuf = mem.map(proto, offset, length);
        } catch (ErrnoException e){
            Log.e(TAG,"could not map, proto: " + proto + ", offset:" +
                       offset +", length: " + length + "\n " + e.getMessage() +
                       "err no. = " + e.errno);
            return null;
        }
        return tempBuf;
    }

    // Reply message to Server
    private void sendMessageToService(int what){
        try {
            Message msg = Message.obtain();
            msg.what = what;
            msg.replyTo = mLocalMessenger;
            mServiceMessenger.send(msg);
        } catch (RemoteException e) {
            Log.e(TAG, "Error in sending message: " + e.getMessage());
        }
    }

    // Finalize no more used SharedMemory
    private void alloverNow() {
        // Notify Service that we are done
        sendMessageToService(SHMService.MSG_DETACH);
        sendMessageToService(SHMService.MSG_DETACH2);
        // unmap ByteBuffers
        if (mBuf1 != null) SharedMemory.unmap(mBuf1);
        if (mBuf2 != null) SharedMemory.unmap(mBuf2);
        // Close SharedMemory
        myShared1.close();
        myShared2.close();
        mBuf1 = null;
        mBuf2 = null;
        myShared1 = null;
        myShared2 = null;
        // Disconnect from Service
        releaseService();
    }
}

*** Point 13 *** When exporting an APK, sign the APK using the same developer key as the destination application.

_images/image35.png

Fig. 4.11.5 Signing the APK with the same developer key as the destination application

4.11.3. Rule Book

When using SharedMemory, the rules contained in the rule book (4.4.2. Rule Book) for the service must be observed. In addition to the rule book, the following rules must also be observed.

  1. Permissions are set properly by the side providing the shared memory for allowing access by the using side (required)
  2. All data in the shared memory is designed assuming that it will be read by sharing applications (required)

4.11.3.1. Permissions are set properly by the side providing the shared memory for allowing access by the using side (required)

When memory is shared, in the design of operations allowable in the memory, each application must limit operations to the minimum required for preventing leaking, alteration, and corruption of information. Services that create SharedMemory objects can use SharedMemory#setProtect() to limit the allowable operations in the entire shared memory before sharing with other applications. The initial values for the operations allowable in the SharedMemory object are read, write, and execute. Except for special reasons, use of executable memory areas should be avoided in order to prevent execution of invalid code [40]. Also, if other applications need to write to the shared memory, a special-purpose shared memory is created and provided separately for enabling safe sharing of memory.

[40]For some devices (based on the CPU architecture that is used), if a certain memory area is readable, it automatically becomes executable. However, even in these cases, writing can be prohibited for these areas to prevent writing of executable code in these areas by other applications.

The argument of SharedMemory#setProtect() is a logical OR for the bit flags (PROT_READ, PROT_WRITE, PROT_EXEC) corresponding to read, write, and execute, respectively. An example is shown below for allowing reading and writing only for the SharedMemory object shMem.

    shMem.setProtect(PROT_READ | PROT_WRITE)

SharedMemory#map() must be executed beforehand in order to enable access by the client to areas (all or part) within the shared memory. During this process, the allowable operations for the memory are specified by an argument, but operations cannot be specified above those permitted by the service beforehand using SharedMemory#setProtect(). For example, the client cannot specify write operations when the service permits reading only. An example is shown below where the SharedMemory object ashMem provided by the service performs map().

    ByteBuffer mbuf;
    // If the Service only allows READ from ashMem,
    // the following code raises an exception
    mbuf = ashMem.map(offset, length, PROT_WRITE);

At the client side, setProtect() can be called to redo the settings so that operations are allowed for the entire shared memory, but like map(), the settings cannot be made to allow operations above those that were permitted by the service.

4.11.3.2. All data in the shared memory is designed assuming that it will be read by sharing applications (required)

As described above, when memory is shared with other applications, the service can set the access permissions (read, write, execute) for the shared memory beforehand. However, even if the flag is set to PROT_WRITE only to allow writing only, in certain cases, reading of the memory cannot be prohibited. In other words, if the memory management unit (MMU) being used by the device does not support memory access that allows writing only, allowing writing for a certain memory area will also allow reading. It is thought that a large number of devices actually have this configuration, and as a result, design must be performed under the assumption that the contents of the shared memory will be known by other applications.

    // Assume that Service side only allow writing go ashMem
    // by SharedMemory#setProtect(PROT_WRITE).
    // It is most of the case that even the client map with
    // PROT_WRITE, he mapped buffer can be read.
    ByteBuffer buf;
    buf = ashMem.map(offset, length, PROT_WRITE);
    // On most of SoC, read does not cause errors
    int len = buf.getInt();
    byte [] bytes = new byte[len];
    buf.get(bytes);

Although PROT_NONE can be specified for the flag to prevent all operations, this defeats the purpose of having a shared memory.

4.11.4. Advanced Topics

4.11.4.1. Actual State of Shared Memory

Up to this point, the memory-sharing mechanism where memory was shared among multiple applications was described. However, in actuality, shared memory is a mechanism that shares the same physical memory area among multiple processes. Each process maps the shared physical memory area to its own address space for accessing (this is performed by SharedMemory#map()). For Android shared memory, the mapped memory area (for Java language) is a single ByteBuffer object. (If shared memory that exceeds the page size is allocated, typically, the shared physical memory area is not divided into consecutive areas, but instead, it is divided into multiple non-consecutive pages. However, if mapped onto a process address space, the memory area becomes consecutive address spaces.)

_images/image99.png

Fig. 4.11.6 Physical memory and process address space

In Unix-based OS, including the Android OS, the connected terminal, USB device, or other peripheral device is abstracted using the concept of device files, and the device is handled as a virtual file. Shared memory in the Android OS is not an exception to this, and this handling corresponds to the device file /dev/ashmem. When this device file is opened, the file descriptor is returned in the same way as when a normal file is opened, and through this process, the shared memory is accessed. In the same way as normal files, this file descriptor can use mmap() to map to the process address space. In Unix-based OS, mmap() is the standard system call, and it obtains the file descriptors for devices files for a wide range of devices and provides a function for mapping the device to the address space of the calling process. This is also used for the shared memory of the Android OS. The mapped address space is visible as a byte sequence from the program (ByteBuffer for Java as mentioned above, and char * at the C language level).

_images/image100.png

Fig. 4.11.7 Mapping of virtual file and address space

The sharing of memory between processes in this framework is equivalent to sharing the file descriptor of /dev/asmem corresponding to this memory area [41]. As a result, this enables low costs for sharing, and after mapping to the address space of the process, this enables access at the same efficiency as normal memory access.

[41]The file descriptor is a unique value within the process, and so when it is passed to other processes, proper conversion is required, but this does not need to be a consideration at the Android SDK API level.