4.安全使用技术

在 Android 中,有许多特定的安全相关问题仅与活动或 SQLite 等特定技术有关。如果开发人员在设计和编码时对每项技术的不同安全问题了解不足,则可能会出现意外的漏洞。本章将介绍开发人员在使用其应用程序组件时需要了解的不同情形。

4.1.创建/使用活动

4.1.1.示例代码

使用活动的风险和对策因使用活动的方式而异。在本章节中,我们根据活动的使用方式对 4 种类型的活动进行了分类。您可以通过下面显示的图表了解应该创建的活动类型。由于安全编码的最佳做法因活动的使用方式而异,因此我们还将解释活动的实现方法。

表 4.1.1 活动类型的定义
类型 定义
专用活动 不能由其他应用程序启动的活动,因此是最安全的活动
公共活动 可以由大量未指定应用程序使用的活动。
合作伙伴活动 一种只能由受信任合作伙伴公司所做的特定应用程序使用的活动。
内部活动 只能由其他内部应用程序使用的活动。
_images/image34.png

图 4.1.1 选择活动类型的流程图

4.1.1.1.创建/使用私有活动

私有活动是其他应用程序无法启动的活动,因此它是最安全的活动。

当使用仅在应用程序中使用的活动(私有活动)时,只要您使用对类的显式意图,就不必担心意外将其发送到任何其他应用程序。但是,第三方应用程序可能会读取用于启动活动的意图。因此,如果您将敏感信息置于用于启动活动的意图中,则有必要采取对策以确保恶意第三方无法读取该信息。

下面显示了如何创建私有活动的示例代码。

要点(创建活动):

  1. 不要指定 taskAffinity。
  2. 不要指定 launchMode。
  3. 将导出的属性显式设置为 false。
  4. 即使是从同一应用程序发送的意图,也应小心、安全地处理收到的意图。
  5. 由于所有敏感信息在同一应用程序中发送和接收,因此可以发送这些信息。

要将活动设为私有,请将 AndroidManifest.xml 中活动元素的“导出”属性设置为 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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

接下来,我们将展示如何使用私有活动的示例代码。

要点(使用活动):

  1. 请勿为 Intent 设置 FLAG_ACTIVITY_NEW_TASK 标志来启动活动。
  2. 使用指定类的显式 Intent 调用同一应用程序中的活动。
  3. 敏感信息只能由 putExtra() 发送,因为目标活动在同一应用程序中。[1]
  4. 即使接收到的结果数据来自同一应用程序内的活动,也要小心、安全地处理这些数据。
[1]注意:请遵守要点 1、2 和 6,否则第三方可能会读取意向。请参阅 4.1.2.2.4.1.2.3.,了解更多信息。
PrivateUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            Toast.makeText(this, String.format("Received result: \"%s\"", result), Toast.LENGTH_LONG).show();
            break;
        }
    }
}

4.1.1.2.创建/使用公共活动

公共活动是指可以被大量未指定的应用程序所使用的活动。需要了解的是,公共活动可能会接收恶意软件发送的 Intent。

此外,在使用公共活动时,必须注意恶意软件也可以接收或读取发送给它们的 Intent。

创建公共活动的示例代码如下所示。

要点(创建活动):

  1. 将导出的属性显式设置为 true。
  2. 小心、安全地处理收到的意图。
  3. 返回结果时,请勿包含敏感信息。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

接下来是公共活动用户端的示例代码。

要点(使用活动):

  1. 请勿发送敏感信息。
  2. 在接收结果时,请小心、安全地处理数据。
PublicUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.创建/使用合作伙伴活动

合作伙伴活动是只能由特定应用程序使用的活动。它们用于希望安全共享信息和功能的合作伙伴公司之间。

第三方应用程序可能会读取用于启动活动的意图。因此,如果您将敏感信息置于用于启动活动的意图中,则有必要采取对策以确保恶意第三方无法读取该信息。

下面显示了创建合作伙伴活动的示例代码。

要点(创建活动):

  1. 不要指定 taskAffinity。
  2. 不要指定 launchMode。
  3. 不要定义意图筛选器,并将导出的属性显式设置为 true。
  4. 通过预定义的白名单验证请求应用程序的证书。
  5. 即使是从合作伙伴应用程序发出的意图,也应小心、安全地处理接收到的意图。
  6. 仅返回允许向合作伙伴应用程序披露的信息。

请参阅“4.1.3.2.验证申请应用程序”,了解如何通过白名单验证申请。另请参阅“5.2.1.3.如何验证应用程序证书的哈希值”,了解如何验证白名单中指定的目标应用程序的证书哈希值。

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
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.请参阅“3.2 谨慎且安全地处理输入数据。”
        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
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

下面描述了使用合作伙伴活动的示例代码。

要点(使用活动):

  1. 验证目标应用程序的证书是否已在白名单中注册。
  2. 请勿为启动活动的意图设置 FLAG_ACTIVITY_NEW_TASK 标志。
  3. 仅通过 putExtra() 发送允许向合作伙伴活动披露的信息。
  4. 使用显式意图调用合作伙伴活动。
  5. 使用 startActivityForResult() 调用合作伙伴活动。
  6. 即使接收到的结果数据来自合作伙伴应用程序,也要小心、安全地处理这些数据。

请参阅“4.1.3.2.验证申请的应用程序”,了解如何通过白名单验证申请。另请参阅“5.2.1.3.如何验证应用程序证书的哈希值”,以了解如何验证要在白名单中指定的目标应用程序的证书哈希值。

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" >

    <application
        android:allowBackup="false"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        
        <activity
            android:name="org.jssec.android.activity.partneruser.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
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 ? 
                // The certificate hash value of "androiddebugkey" is in debug.keystore.
                "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" : 
                // The 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.请参阅“3.2 谨慎且安全地处理输入数据”。
            Toast.makeText(this,
                    String.format("Received result: \"%s\"", result), Toast.LENGTH_LONG).show();
            break;
        }
    }
}
PkgCertWhitelists.java
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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.创建/使用内部活动

内部活动是指禁止内部应用程序之外的应用程序使用的活动。它们用于内部开发的、需要安全地共享信息和功能的应用程序。

第三方应用程序可能会读取用于启动活动的意图。因此,如果您将敏感信息置于用于启动活动的意图中,则有必要采取对策以确保恶意第三方无法读取该信息。

创建内部活动的示例代码如下所示。

要点(创建活动):

  1. 定义内部签名权限。
  2. 不要指定 taskAffinity。
  3. 不要指定 launchMode。
  4. 请求内部签名权限。
  5. 不要定义意图筛选器,并将导出的属性显式设置为 true。
  6. 验证内部签名权限是否由内部应用程序定义。
  7. 小心、安全地处理接收到的意图,即使是从内部应用程序发出的意图。
  8. 由于请求的应用程序是内部的,因此可以返回敏感信息。
  9. 导出 APK 时,使用与请求应用程序相同的开发人员密钥签署 APK。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 9 *** 导出 APK 时,使用与请求应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.1.2 使用与请求的应用程序相同的开发人员密钥签署 APK

下面描述了使用内部活动的示例代码。

要点(使用活动):

  1. 声明要使用内部签名权限。
  2. 验证内部签名权限是否由内部应用程序定义。
  3. 验证目标应用程序是否是使用内部证书签署。
  4. 敏感信息只能由 putExtra() 发送,因为目标应用程序是内部应用程序。
  5. 使用显式意图调用内部活动。
  6. 即使接收到的数据来自内部应用程序,也要小心、安全地处理这些数据。
  7. 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。
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" >

    <!-- *** 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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            Toast.makeText(this, String.format("Received result: \"%s\"", result), Toast.LENGTH_LONG).show();
            break;
        }
    }
}
SigPerm.java
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 16 *** 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.1.3 使用与目标应用程序相同的开发人员密钥签署 APK

4.1.2.规则手册

在创建活动或向活动发送意图时,请务必遵循以下规则。

  1. 仅用于应用程序内部的活动必须设置为“私有”(必需)
  2. 请勿指定 taskAffinity(必需)
  3. 请勿指定 launchMode(必需)
  4. 请勿为启动活动的意图设置 FLAG_ACTIVITY_NEW_TASK 标志(必需)
  5. 小心、安全处理接收到的意图(必需)
  6. 在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)
  7. 返回结果时,请注意目标应用程序中该结果信息泄漏的可能性(必需)
  8. 如果预定了目标活动,请使用显式意图。(必需)
  9. 小心、安全地处理请求的活动返回的数据(必需)
  10. 如果链接到另一公司的应用程序,请验证目标活动(必需)
  11. 转手提供资产时,应使用相同的保护级别保护资产(必需)
  12. 应尽可能限制发送敏感信息(推荐)

4.1.2.1.仅用于应用程序内部的活动必须设置为“私有”(必需)

仅在单个应用程序中使用的活动不需要从其他应用程序接收任何意向。开发人员通常假定私有活动不会受到攻击,但有必要将这些活动显式设置为私有,以阻止收到恶意意图。

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

不应对仅在单个应用程序中使用的活动设置意图筛选器。由于意图过滤器的特征和其工作方式的特征,即使您打算将意图发送至私有活动,但如果您通过意图过滤器发送意图,则可能无意中启动另一个活动。请参阅高级主题“4.1.3.1.组合导出的属性和意图过滤器设置(用于活动)”,以了解更多详细信息。

AndroidManifest.xml(Not recommended)
       <!-- Private activity -->
       <!-- *** 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.请勿指定 taskAffinity(必需)

在 Android 操作系统中,活动由任务管理。任务名称由根活动具有的相关性确定。另一方面,对于根活动以外的活动,活动所属的任务不是仅由相关性确定,也取决于活动的启动模式。请参阅“4.1.3.4.根活动”,以了解更多详细信息。

在默认设置中,每个活动都使用其程序包名称作为其相关性。因此,任务是根据应用程序分配的,所以单个应用程序中的所有活动都属于同一任务。要更改任务分配,您可以在 AndroidManifest.xml 文件中明确声明相关性,也可以在发送到活动的意图中设置标志。但是,如果您更改任务分配,则另一个应用程序可能读取发送到属于另一个任务的活动的意向。

请确保在 AndroidManifest.xml 文件中不指定 android:taskAffinity,并使用默认设置,将相关性保持为软件包名称,以防止发送或接收的意图中的敏感信息被其他应用程序读取。

下面是用于创建和使用私有活动的 AndroidManifest.xml 文件示例。

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

        <!-- *** 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 -->
        <!-- *** POINT 1 *** Do not specify taskAffinity -->
        <activity
            android:name=".PrivateActivity"
            android:label="@string/app_name"
            android:exported="false" />
     </application>

请参阅“Google Android 编程指南”[2]、Google 开发人员 API 指南“任务和反堆栈”[3]、“4.1.3.3.读取发送至活动的意图”和“4.1.3.4.根活动”,以了解有关任务和相关性的更多详细信息。

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

4.1.2.3.请勿指定 launchMode(必需)

活动启动模式用于控制启动活动时创建新任务和活动实例的设置。默认情况下,它设置为“标准”。在“标准”设置中,启动活动时始终创建新实例,任务遵循属于调用活动的任务,并且不能创建新任务。创建新任务时,其他应用程序可能会读取调用意图的内容,因此当意图中包括敏感信息时,需要使用“标准”活动启动模式设置。

可以在 AndroidManifest.xml 文件中的 android:launchMode 属性中显式设置活动启动模式,但由于上述原因,不应在活动声明中设置此模式,且该值应保留为默认的“标准”。

AndroidManifest.xml
        <!-- *** 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 -->
        <!-- *** POINT 2 *** Do not specify launchMode -->
        <activity
            android:name=".PrivateActivity"
            android:label="@string/app_name"
            android:exported="false" />
    </application>

请参阅“4.1.3.3.读取发送至活动的意图”和“4.1.3.4.根活动”。

4.1.2.4.不要为启动活动的意图设置 FLAG_ACTIVITY_NEW_TASK 标志(必需)

在执行 startActivity() 或 startActivityForResult() 时,可以更改活动的启动模式,在某些情况下可能会生成新任务。因此,在执行期间,请不更改活动的启动模式。

要更改活动启动模式,请使用 setFlags() 或 addFlags(), 设置 Intent 标志,并将该 Intent 用作 startActivity() 或 startActivityForResult() 的参数。FLAG_ACTIVITY_NEW_TASK 是用于创建新任务的标志。设置 FLAG_ACTIVITY_NEW_TASK 后,如果被调用的活动在后台或前台不存在,则将创建新任务。

FLAG_ACTIVITY_MULTIPLE_TASK 标志可以与 FLAG_ACTIVITY_NEW_TASK 同时设置。在这种情况下,将始终创建新任务。可以使用任一设置创建新任务,因此不应将这些任务设置为处理敏感信息的 Intent。

发送 intent 的示例

        Intent intent = new Intent();

        // *** 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);

此外,您可能会认为,即使已通过显式设置 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 标志创建了新任务,也有一种方法可以防止读取 Intent 的内容。但是,即使使用此方法,内容也可以由第三方读取,因此您应该避免使用 FLAG_ACTIVITY_NEW_TASK。

请参阅“4.1.3.1.合并导出的属性和意图过滤器设置(用于活动)”、“4.1.3.3.读取发送至活动的意图”和“4.1.3.4.Root 活动”。

4.1.2.5.小心、安全处理接收到的意图(必需)

风险因活动类型而异,但在处理收到的意图数据时,您首先应该做的是输入验证。

由于公共活动可以接收来自不受信任来源的意图,因此它们可能会受到恶意软件的攻击。另一方面,私有活动永远不会直接从其他应用程序接收任何意图,但目标应用程序中的公共活动可能会将恶意意图转发到私人活动,因此您不应假定私人活动无法接收任何恶意输入。由于合作伙伴活动和内部活动也存在被转发恶意意图的风险,因此也有必要对这些意图执行输入验证。

请参阅“3.2.谨慎且安全地处理输入数据

4.1.2.6.在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)

创建活动时,请确保通过定义内部签名权限来保护内部活动。由于在 AndroidManifest.xml 文件中定义权限或声明权限请求不能提供足够的安全性,请务必参阅“5.2.1.2.如何使用内部定义的签名权限在内部应用程序之间通信。”

4.1.2.7.返回结果时,请注意目标应用程序中该结果信息泄漏的可能性(必需)

使用 setResult() 返回数据时,目标应用程序的可靠性将取决于活动类型。当使用公共活动返回数据时,目标系统可能会变成恶意软件,在这种情况下,信息可能会以恶意方式使用。对于私人活动和内部活动,无需担心返回的数据被恶意使用,因为它们将返回到您控制的应用程序中。合作伙伴活动在一定程度上处于中间阶段。

如上所述,当从活动返回数据时,您需要注意目标应用程序中的信息泄露。

返回数据的示例。

    public void onReturnResultClick(View view) {

        // *** 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.如果预定了目标活动,请使用显式意图。(必需)

使用隐式 Intent 的活动时,作为 Intent 发送目标的活动由 Android 操作系统确定。如果将 Intent 误发送给恶意软件,则会发生信息泄露。另一方面,当使用显式 Intent 活动时,只有目标活动会收到 Intent,这样就更安全了。

除非用户绝对有必要确定应将 Intent 发送到哪个应用程序的活动,否则应使用显式 Intent 并提前指定目标。

在同一应用程序中按显式 Intent 使用活动

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

按显式 Intent 使用其他应用程序的公共活动

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

但是,即使按显式 Intent 使用其他应用程序的公共活动,目标活动也可能是恶意软件。这是因为,即使您按数据包名称限制目标,恶意应用程序仍可能假冒与实际应用程序相同的数据包名称。要消除此类风险,必须考虑使用合作伙伴或内部人员。

请参阅“4.1.3.1.合并导出的属性和意图过滤器设置(用于活动)”。

4.1.2.9.小心、安全地处理请求的活动返回的数据(必需)

虽然根据您访问的活动类型,风险略有不同,但在处理作为返回值接收的 Intent 数据时,您始终需要对收到的数据执行输入验证。

公共活动必须接受来自不受信任来源的返回 Intent,因此在访问公共活动时,返回的 Intent 可能实际上是由恶意软件发送的。通常错误地认为,私人活动返回的所有 Intent 都是安全的,因为它们来自同一应用程序。但是,由于可能会间接转发从不受信任来源收到的 Intent,因此您不应盲目信任该 Intent 的内容。合作伙伴和内部活动面临的风险介于私人和公共活动之间。请确保对这些活动也执行输入验证。

请参阅“3.2.谨慎且安全地处理输入数据”,了解更多信息。

4.1.2.10.如果链接到其他公司的应用程序,请验证目标活动(必需)

在链接到其他公司的应用程序时,请确保有白名单。为此,您可以在应用程序中保存公司证书哈希的副本,并使用目标应用程序的证书哈希进行检查。这将防止恶意应用程序冒充发送 Intent。请参阅示例代码部分“4.1.1.3.创建/使用合作伙伴活动”了解具体的实施方法。有关技术详细信息,请参阅“4.1.3.2.验证请求应用程序。”

4.1.2.11.转手提供资产时,应使用相同的保护级别保护资产(必需)

如果将受权限保护的信息或功能资产转手提供给另一个应用程序,您需要确保它具有访问该资产所需的相同权限。在 Android 操作系统权限安全模型中,只有获得适当权限的应用程序才能直接访问受保护的资产。但是,这存在漏洞,因为对资产具有权限的应用程序可以充当代理并允许访问无特权应用程序。实质上,这与重新委派权限相同,因此被称为“权限重新委派”问题。请参阅“5.2.3.4.权限重新委派问题。”

4.1.2.12.应尽可能限制发送敏感信息(推荐)

您不应将敏感信息发送给不受信任方。即使您链接到特定应用程序,您仍有可能无意中将 Intent 发送到不同的应用程序,或者恶意第三方可能窃取您的 Intent。请参阅“4.1.3.5.使用活动时的日志输出。”

在向活动发送敏感信息时,您需要考虑信息泄露的风险。您必须假定恶意第三方可以获取 Intent 中发送到公共活动的所有数据。此外,在将 Intent 发送给合作伙伴或内部活动时,也会存在各种信息泄露风险,具体取决于实施情况。即使将数据发送到私人活动,但可能会有通过 LogCat 泄露 Intent 中的数据的风险。Intent 附加部分中的信息不会输出到 LogCat,因此最好在其中存储敏感信息。

但是,不发送敏感数据是防止信息泄露的唯一完美解决方案,因此您应尽可能限制发送的敏感信息量。当需要发送敏感信息时,最佳做法是仅发送到受信任的活动,并确保不会通过 LogCat 泄露信息。

此外,不应将敏感信息发送到 Root 活动。Root 活动是在创建任务时首先调用的活动。例如,从启动器启动的活动始终是 Root 活动。

请参阅“4.1.3.3.读取发送至活动的意图”和“4.1.3.4.Root 活动”了解有关 Root 活动的更多详情。

4.1.3.高级主题

4.1.3.1.组合导出的属性和意图过滤器设置(用于活动)

我们已在本指南中说明了如何实施这四种类型的活动:私有活动、公共活动、合作伙伴活动和内部活动。对于在 AndroidManifest.xml 文件中定义的导出属性的每种类型和在下表中定义的意图过滤器元素,有各种允许的设置组合。请验证导出的属性和意图过滤器元素与您尝试创建的活动的兼容性。

表 4.1.2 已导出的属性和意图过滤器的组合
  导出属性的值
true false 未指定
已定义意图过滤器 公共 (请勿使用) (请勿使用)
未定义意图过滤器 公共、合作伙伴、内部 私有 (请勿使用)

如果未指定活动的导出属性,则该活动是否为公开活动取决于该活动是否存在意图筛选器。[4] 但是,在此指南中,禁止将导出的属性设置为未指定。通常,如之前所述,最好避免使用依赖于任何给定 API 的默认行为的实施;此外,如果存在用于配置重要安全相关设置——如导出的属性——的显式方法,则最好始终使用这些方法。

[4]如果定义了任何意图过滤器,则活动为公共;否则为专用。更多信息,请参阅https://developer.android.com/guide/topics/manifest/activity-element.html#exported.

不应使用未定义的意图过滤器和导出的 false 属性的原因是 Android 行为中存在漏洞,并且由于意图过滤器的工作方式,可能会意外调用其他应用程序的活动。下面的两个图显示了此说明。图 4.1.4 是一个正常行为的示例,其中只有来自同一应用程序的隐式意图才能调用私有活动(应用程序 A)。意图过滤器(操作 = “X”)定义为仅在应用程序 A 中工作,因此这是预期行为。

_images/image36.png

图 4.1.4 正常行为示例

下面的图 4.1.5 显示了在应用程序 B 和应用程序 A 中定义相同的意图过滤器(操作 =“X”)的情况。应用程序 A 尝试通过发送隐式意图在同一应用程序中调用私有活动,但此时会出现一个对话框,询问用户要选择哪个应用程序,同时应用程序 B 中由于用户选择错误调用了公共活动 B-1。由于存在漏洞,可能会将敏感信息发送到其他应用程序或者应用程序会收到意外的重新调整值。

_images/image37.png

图 4.1.5 异常行为的示例

如上所示,使用意图过滤器将隐式意图发送至私有活动可能导致意外行为,因此最好避免此设置。此外,我们已验证此行为不依赖于应用程序 A 和应用程序 B 的安装顺序。

4.1.3.2.验证请求应用程序

此处我们将介绍有关如何实施合作伙伴活动的技术信息。合作伙伴应用程序仅允许在白名单中注册的特定应用程序访问,并拒绝所有其他应用程序。由于内部应用程序以外的应用程序也需要访问权限,因此我们不能使用签名权限进行访问控制。

简单地说,我们希望验证尝试使用合作伙伴活动的应用程序,方法是检查该应用程序是否已在预定义的白名单中注册,如果已注册,则允许访问;如果未注册,则拒绝访问。应用程序验证是通过从请求访问的应用程序获取证书并将其哈希与白名单中的证书进行比较来完成的。

有些开发人员可能认为仅仅比较软件包名称就足够了,而不必获取证书。但是,合法应用程序的软件包名称很容易造假,因此这不是检查可靠性的好方法。不应使用任意可分配的值进行身份验证。另一方面,由于只有应用程序开发人员才有用于签署其证书的开发人员密钥,这是一种更好的识别方法。由于证书不容易被造假,除非恶意第三方可以窃取开发人员密钥,否则恶意应用程序受到信任的可能性很小。虽然可以将整个证书存储在白名单中,但只存储 SHA-256 哈希值就已足够,以便将文件大小降到最低。

使用此方法有两个限制。

  • 请求应用程序必须使用 startActivityForResult(),而不是 startActivity()。
  • 请求的应用程序只能从活动调用。

第二个限制是由于第一个限制而施加的限制,因此在技术上只有一个限制。

此限制由于获取调用应用程序的程序包名称的活动 Activity.getCallingPackage() 的限制而发生。Activity.getCallingPackage() 仅在 startActivityForResult() 调用源(请求)应用程序时才返回源(请求)应用程序的程序包名称,但遗憾的是,当 startActivity() 调用时,它仅返回空值。因此,当使用此处说明的方法时,源(请求)应用程序需要使用 startActivityForResult(),即使它不需要获取返回值。此外,startActivityForResult() 只能在活动类中使用,因此源(请求者)仅限于活动。

PartnerActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据。”
        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
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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.读取发送到活动的意图

在 Android 5.0(API 等级 21)及更高版本中,使用 getRecentTasks 检索的信息已限制为调用方自己的任务,并且可能是一些其他已知不敏感的任务。但是,支持 Android 5.0(API 等级 21)下的版本的应用程序应能防止泄露敏感信息。

下面描述了 Android 5.0 和更早版本中出现的此问题的内容。

发送到任务根活动的意图将添加到任务历史记录中。根活动是任务中开始的第一个活动。任何应用程序都可以使用 ActivityManager 类读取添加到任务历史记录中的意图。

下面显示了从应用程序读取任务历史记录的示例代码。要浏览任务历史记录,请在 AndroidManifest.xml 文件中指定 GET_TASKS 权限。

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
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());
            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());
                }
            }
        }
    }
}

您可以使用 ActivityManager 类的 getRecentTasks() 函数获取任务历史记录的指定条目。每个任务的信息存储在 ActivityManager.RecentTaskInfo 类的实例中,但发送到任务根活动的意图存储在其成员变量 baseIntent 中。由于根活动是在创建任务时启动的活动,因此在调用活动时,请确保不要满足以下两个条件。

  • 调用活动时将创建新任务。
  • 调用的活动是任务已存在于后台或前台的根活动。

4.1.3.4.根活动

根活动是指作为任务起点的活动。换言之,这是创建任务时启动的活动。例如,当启动器启动默认活动时,此活动将是根活动。根据 Android 规范,可以从任意应用程序读取发送到根活动的意图的内容。因此,有必要采取对策,确保不要向根活动发送敏感信息。在本指南中,我们制定了以下三条规则,以避免将调用活动变成根活动。

  • 不应指定 taskAffinity。
  • 不应指定 launchMode。
  • 不应在发送至活动的意图中设置 FLAG_ACTIVITY_NEW_TASK 标志。

我们认为在以下情况中活动可能成为的根活动。调用活动成为根活动取决于以下因素。

  • 调用活动的启动模式
  • 调用活动的任务及其启动模式

首先,让我解释一下“活动的启动模式”。可以通过在 AndroidManifest.xml 中编写 android:launchMode 来设置活动的启动模式。如果未写入,则将其视为“标准”。此外,还可以通过设置为意图的标志来更改启动模式。标志“FLAG_ACTIVITY_NEW_TASK”通过“singleTask”模式启动活动。

可以指定的启动模式如下所示。我将主要介绍与根活动的关系。

标准

此模式调用的活动不是根活动,并且属于调用端任务。每次调用时,都会生成活动实例。

singleTop

此启动模式与“标准”模式相同,只是在启动前台任务最前端显示的活动时不会生成实例。

singleTask

此启动模式确定任务按相关性值属于的活动。当后台或前台都不存在与活动相关性匹配的任务时,任务将与活动实例一起生成新任务。当任务存在时,不会生成任何一项。在以前的实例中,启动活动的实例成为根。

singleInstance

与“singleTask”相同,但以下几点不同。只有根活动可以属于新生成的任务。因此,通过此模式启动的活动实例始终是根活动。现在,我们需要注意的是,调用活动的类名称和包括在任务中的活动的类名称是不同的,尽管具有相同名称的调用活动相关性的任务已存在。

从上面的内容中,我们可以了解由“singleTask”或“singleInstance”启动的活动可能成为根。为确保应用程序的安全,不应通过这些模式启动应用程序。

接下来,我将介绍“活动的任务及其启动模式”。即使活动是通过“标准”模式调用的,它在某些情况下也会成为根活动,具体取决于活动所属的任务状态。

例如,请考虑名调用活动的任务已经在后台运行的情况。

此处的问题是任务的活动实例由“singleInstance”启动。当“标准”调用的活动的相关性与任务相同时,现有“singleInstance”活动的限制将生成新任务。但是,当每个活动的类名称相同时,不会生成任务,将使用现有活动实例。在任何情况下,调用活动都将成为根活动。

如上所述,调用根活动的情况非常复杂,例如,它取决于执行状态。因此,在开发应用程序时,最好通过“标准”模式设计活动。

例如,发送到私有活动的意图是从其他应用程序中读出的,示例代码显示私有活动的调用方是通过“singleInstance”模式启动的。在此示例代码中,私有活动通过“标准”模式启动,但此私有活动由于调用方活动的“singleInstance”条件,将成为新任务的根活动。此时,发送到私有活动的敏感信息将记录在任务历史中,以便可以从其他应用程序中读取。还有一点,调用方活动和私有活动具有相同的相关性。

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>

私人活动仅将结果返回至接收的意图。

PrivateActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

在私人活动的呼叫方端,私人活动通过“标准”模式启动,而不将标志设置为“意图”。

PrivateUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            Toast.makeText(this, String.format("Received result: \"%s\"", result), Toast.LENGTH_LONG).show();
            break;
        }
    }
}

4.1.3.5.使用活动时的日志输出

使用活动时,ActivityManager 会将意图的内容输出到 LogCat。以下内容将输出到 LogCat,因此在这种情况下,此处不应包括敏感信息。

  • 目标包名称
  • 目标类名称
  • Intent#setData() 设置的 URI

例如,当应用程序发送邮件时,如果应用程序将邮件地址指定为 URI,则邮件地址将输出到 LogCat。因此,最好通过设置附加项来发送。

按如下方式发送邮件时,邮件地址将显示在 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);

使用“附加项”时,邮件地址不再显示在 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);

但是,在某些情况下,其他应用程序可以使用 ActivityManager#getRecentTasks() 读取意图的附加数据。请参阅“4.1.2.2.请勿指定 taskAffinity(必需)”、“4.1.2.3.请勿指定 launchMode(必需)和“4.1.2.4.请勿为启动活动的意图设置 FLAG_ACTIVITY_NEW_TASK 标志(必需)”。

4.1.3.6.在 PreferenceActivity 中防止片段注入

当从 PreferenceActivity 派生的类为公共活动时,可能会出现一个称为片段注入 [5] 的问题。为防止出现此问题,必须覆盖 PreferenceActivity.IsValidFragment(),并检查其参数的有效性,以确保活动不会在无意中处理任何片段。(有关输入数据安全性的更多信息,请参阅“3.2 谨慎且安全地处理输入数据”一节。)

[5]有关片段注入的更多信息,请参阅此网址:https://securityintelligence.com/new-vulnerability-android-framework-fragment-injection/

下面显示了已覆盖 IsValidFragment() 的示例。请注意,如果源代码被混淆,类名称和参数值比较的结果可能会更改。在这种情况下,有必要采取替代对策。

覆盖 isValidFragment() 方法示例

    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);
    }

请注意,如果应用程序的 targetSdkVersion 为 19 或更高版本,无法覆盖 PreferenceActivity.isValidFragment(),将导致安全异常,且每当注入片段[ValidFragment() 被调用时]时,应用程序终端将终止,因此在这种情况下,覆盖 PreferenceActivity.isValidFragment() 是必须要做的。

4.1.3.7.自动填充框架

在 Android 8.0(API 等级 26)中添加了自动填充框架。使用此框架,应用程序可以存储用户输入的信息,如用户名、密码、地址、电话号码和信用卡,并在必要时检索此信息,以允许应用程序自动填写表单。这是一种可减轻用户数据输入负担的方便机制;但是,由于它允许给定应用程序将密码和信用卡等敏感信息传递给其他应用程序,因此必须谨慎处理。

框架概述
2 个组件

在下面的内容中,我们概述了自动填充框架注册的两个组件[6]

  • 符合自动填充条件的应用程序(用户应用程序):
    • 将视图信息(文本和属性)传递至自动填充服务;根据需要从自动填充服务接收信息以自动填充表单。
    • 所有具有活动的应用程序都是用户应用程序(在前台时)。
    • 所有用户应用程序的所有视图都可以自动填充。还可以明确指定任何给定的单个视图不符合自动填充条件。
    • 也可以将应用程序的“自动填充”限制在同一软件包内使用“自动填充”服务。
  • 提供自动填充的服务(自动填充服务):
    • 保存应用程序传递的“查看”信息(需要用户权限);为应用程序提供视图中(候选列表)自动填充所需的信息
    • 符合保存此信息条件的视图由自动填充服务确定。(在自动填充框架内,默认情况下,有关活动中包含的所有视图的信息都将传递到自动填充服务。)
    • 还可以制作由第三方提供的自动填充服务。
    • 一个终端中可能存在多个服务,并且只有用户通过启用“设置”选择的服务(“无”也是可能的选择。)
    • 服务还可以提供 UI 以通过密码输入或其他机制验证用户,从而保护处理的用户信息的安全性。
[6]用户应用程序自动填充服务可能属于同一个软件包(同一 APK 文件)或不同的软件包。
自动框架的程序流程图

图 4.1.6 流程图说明了在自动填充过程中与自动填充相关的组件之间交互的程序流程。当由焦点在用户应用程序视图中移动等事件触发时,有关该视图的信息(主要是父 - 子关系和视图的各种属性)将通过自动填充框架传递到在“设置”中选择的自动填充服务。根据其接收的数据,自动填充服务从数据库获取自动填充所需的信息(候选列表),然后将此信息返回框架。框架向用户显示候选列表,应用程序使用用户选择的数据执行自动填充操作。

_images/image38.png

图 4.1.6 自动填充组件之间的程序流程

下一步,图 4.1.7 流程图说明了通过自动填充保存用户数据的程序流程。在通过调用 AutofillManager#commit() 触发事件或活动未设定焦点时,如果修改了视图的任何自动填充值,并且用户已通过自动填充框架显示的“保存权限”对话框授予权限,则通过“自动填充”框架将视图(包括文本)上的信息传递至通过“设置”选择的“自动填充”服务,并且自动填充服务将信息存储在数据库中以完成程序序列。

_images/image39.png

图 4.1.7 保存用户数据组件之间的程序流程

自动填充用户应用程序的安全问题

如上面“框架概述”一节中所述,自动填充框架采用的安全模型是基于一定的条件的,它是在假设用户配置设置以选择安全自动填充服务并对存储数据时要将哪些数据传递到哪些自动填充服务作出适当决策的前提下。

但是,如果用户无意中选择了不安全的自动填充服务,则用户可能允许存储不应传递到自动填充服务的敏感信息。接下来我们将讨论此类情形可能导致的损害。

保存信息时,如果用户选择了自动填充服务并通过保存权限对话框授予了该服务的权限,则当前使用中的应用程序显示的活动中包含的所有视图的信息都可能会传递到自动填充服务。如果自动填充服务是恶意软件,或者出现其他安全问题,例如,如果自动填充服务将查看信息存储在外部存储介质或不安全的云服务上,则可能会造成应用程序处理的信息被泄露的风险。

另一方面,在自动填充期间,如果用户选择了一个恶意软件作为自动填充服务,则恶意软件传输的值作为输入内容进入应用程序。此时,如果应用程序或应用程序向其发送数据的云服务未充分验证数据输入的安全性,则可能出现信息泄露和/或应用程序或服务终止的风险。

请注意,如上面的“2 组件”部分所述,具有活动的应用程序自动符合自动填充条件,因此所有具有活动的应用程序开发人员在设计和实施应用程序时必须考虑上述风险。在接下来的内容中,我们将提出缓解上述风险的对策,我们建议根据应用程序要求采取对策,请参阅“3.1.3.资产分类和保护对策”和其他相关资源。

降低风险的步骤:1

如上所述,自动填充框架内的安全性最终仅由用户自行决定。因此,应用程序可用的对策范围在一定程度上有限。但是,有一种方法可以缓解上述问题:将视图的 importantForAutofill 属性设置为“no”可确保不会将任何视图信息传递至自动填充服务(即,视图不符合“自动填充”条件),即使用户无法做出适当的选择或权限(例如选择一个恶意软件作为自动填充服务)也是如此。[7]

[7]即使在执行此步骤后,在某些情况下也可能无法避免上述安全问题,例如,如果用户有意使用自动填充。实施“降低风险的步骤:2”中描述的步骤将在这些情况下提高安全性。

importantForAutofill 属性可以通过以下任何方法指定。

  • 在布局 XML 中设置 importantForAutofill 属性
  • 调用 View#setImportantForAutofill()

可为此属性设置的值如下所示。确保使用适合指定范围的值。特别要注意的是,当视图的值设置为“否”时,该视图将不符合自动填充条件,但其子项仍可用于自动填充。默认值为“自动”。

表 4.1.3 可用于自动填充?
常量名称
指定视图 子视图
“自动”
IMPORTANT_FOR_AUTOFILL_AUTO
“自动”[8] “自动”[8]
“否”
IMPORTANT_FOR_AUTOFILL_NO
“noExcludeDescendants”
IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
“是”
IMPORTANT_FOR_AUTOFILL_YES
“yesExcludeDescendants”
IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS
[8](1, 2) 由自动填充框架确定

也可以使用 AutofillManager#hasEnabledAutofillServices(),将自动填充功能限制为在同一软件包中使用自动填充服务。

接下来,我们显示了一个示例,只有当设置已配置为使用同一数据包中的自动填充服务时,活动中的所有视图才有资格使用自动填充(视图是否实际使用自动填充由自动填充服务决定)。也可以为各个视图调用 View#setImportantForAutofill()。

DisableForOtherServiceActivity.java
package org.jssec.android.autofillframework.autofillapp;

import android.os.Bundle;
import android.support.v7.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();

        if (!mIsAutofillEnabled) {
            View rootView = this.getWindow().getDecorView();
            //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
            View rootView = this.getWindow().getDecorView();
            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);
    }
}
降低风险的步骤:2

即使在应用程序已实施上一节(“降低风险的步骤:1”)中描述的步骤的情况下,用户也可以通过长按视图、显示浮动工具栏或类似的控制界面并选择“自动输入”来强制启用“自动填充”。在这种情况下,所有视图的信息—包括那些已经将 importantForAutofill 属性设置为“否”或已执行类似步骤的视图—都将传递到自动填充服务。

通过从浮动工具栏菜单和其他控制界面中删除“自动输入”选项,即使在此类情况下也可以避免信息泄露的风险;除“降低风险的步骤:1”中的步骤之外,此步骤也需要实施。

用于此目的的示例代码如下所示。

DisableAutofillActivity.java
package org.jssec.android.autofillframework.autofillapp;

import android.os.Bundle;
import android.support.v7.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("");
    }

}
降低风险的步骤:3

在 Android 9.0(API 等级 28)中,可以使用 AutofillManager#getAutofillServiceComponentName() 来查找当前启用了哪些自动填充服务组件。这可用于获取包名并确认应用程序本身是否被视为可信的自动填充服务。

在这种情况下,如“4.1.3.2.验证请求应用程序”中所述,由于包名可能造假,因此不建议仅使用此方法进行身份验证。与 4.1.3.2. 中描述的示例相同,自动填充服务证书必须从包名中获得,必须通过检查证书是否与白名单中预先注册的证书相匹配来验证身份。该方法的详细描述在 4.1.3.2.,有关更多信息,请参阅本节。

下面显示了一个示例,其中自动填充仅在白名单中预先注册的自动填充服务启用时才用于活动的所有视图。

EnableOnlyWhitelistedServiceActivity.java
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) {
        isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();
	// Register hash value of the certificate of trusted Autofill Service
        sWhitelists.add("org.jssec.android.autofillframework.autofillservice", isdebug ? 
	    // Hash value of the certificate "androiddebugkey" in debug.keystore
            "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" : 
	    // Hash value of the certificate "partnerkey" in keystore
            "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");
        // 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();

        if (!mIsAutofillEnabled) {
            View rootView = this.getWindow().getDecorView();
            // 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
            View rootView = this.getWindow().getDecorView();
            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) {
            mIsAutofillEnabled = false;// "Settings" ‐"Autofill Service" is set to "None"
            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();
                mIsAutofillEnabled = false;    // if not on white list, do not use Autofill Service
            }
            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
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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.接收/发送广播消息

4.2.1.示例代码

必须创建广播接收器才能接收广播。使用广播接收器的风险和对策因接收的广播类型而异。

您可以根据以下判断流程中找到需要的广播接收器。接收应用程序无法检查与合作伙伴链接所必需的广播发送应用程序的程序包名称。因此,无法创建合作伙伴的广播接收器。

表 4.2.1 广播接收器类型的定义
类型 定义
专用广播接收器 广播接收器只能接收来自同一应用程序的广播,因此是最安全的广播接收器
公共广播接收器 这种广播接收器可以接收来自大量未指定应用程序的广播。
内部广播接收器 只能从其它内部应用程序接收广播的广播接收器
_images/image40.png

图 4.2.1 选择广播接收器类型的流程图

此外,广播接收器可根据定义方法分为 2 种类型:静态广播接收器和动态广播接收器。它们之间的差异可在下图中找到。在示例代码中,将显示每种类型的实施方法。我们还介绍了发送应用程序的实施方法,因为发送信息的对策是根据接收器确定的。

表 4.2.2 广播接收器的定义方法和特性
  定义方法 特性
静态广播接收器 通过在 AndroidManifest.xml 中编写 <receiver> 元素进行定义
  • 某些广播消息具有一定的限制(例如,无法接收系统发送的 ACTION_BATTERY_CHANGED)。
  • 在卸载之前,可以从应用程序的初始启动接收广播。
  • 如果应用程序的 targetSDKVersion 为 26 或更高版本,则在运行 Android 8.0 的终端(API 等级 26)或更高版本的终端上,可能没有为隐式广播意图注册广播接收器 [9]
动态广播接收器 通过在程序中调用 registerReceiver() 和 unregsterReceiver(),可动态注册/取消注册广播接收器。
  • 可以接收静态广播接收器无法接收的广播。
  • 接收广播信息的时间可以由程序控制。例如,只能在活动位于前端时接收广播信息。
  • 无法创建私有广播接收器。
[9]作为此规则的例外情况,系统发送的某些隐式广播意图可能使用广播接收器。有关详细信息,请参阅以下网址。https://developer.android.com/guide/components/broadcast-exceptions.html

4.2.1.1.私有广播接收器 - 接收/发送广播

“私有广播接收器”是最安全的广播接收器,因为只能接收从应用程序内部发送的广播信息。无法将动态广播接收器注册为“私有”,因此私有广播接收器仅包含静态广播接收器。

要点(接收广播信息):

  1. 将导出的属性显式设置为 false。
  2. 即使是从同一应用程序发送的意图,也应小心、安全地处理收到的意图。
  3. 敏感信息可作为返回的结果发送,因为请求来自同一应用程序。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

下面显示了用于将广播信息发送到私有广播接收器的示例代码。

要点(发送广播讯息):

  1. 使用指定了类的显式意图以便在同一应用程序中调用接收器。
  2. 由于目标接收器在同一应用程序中,因此可以发送敏感信息。
  3. 即使接收器中的数据来自同一应用程序,也应小心、安全地处理接收到的结果数据。
PrivateSenderActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.公共广播接收器 - 接收/发送广播消息

公共广播接收器是一种可以接收来自大量未指定的应用程序的广播的接收器,因此需要注意它可能接收来自恶意软件的广播。

要点(接收广播信息):

  1. 将导出的属性显式设置为 true。
  2. 小心、安全地处理收到的意图。
  3. 返回结果时,请勿包含敏感信息。

公共接收器是公共广播接收器的示例代码,可在静态广播接收器和动态广播接收器中使用。

PublicReceiver.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

静态广播接收器在 AndroidManifest.xml 中定义。请注意,根据终端版本的不同,隐式广播意图的接收可能会受到限制,如表 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>

在动态广播接收器中,通过调用程序中的 registerReceiver() 或 unregisterReceiver() 可执行注册/取消注册。为了通过按钮操作执行注册/取消注册,该按钮在 PublicReceiverActivity 上分配。由于动态广播接收器实例的范围比 PublicReceiverActivity 长,因此无法将其作为 PublicReceiverActivity 的成员变量保留。在此情况下,请将动态广播接收器实例保持为 DynamicReceiverService 的成员变量,然后从 PublicReceiverActivity 启动/结束 DynamicReceiverService,以间接注册/取消注册动态广播接收器。

DynamicReceiverService.java
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);
        filter.setPriority(1);  // Prioritize Dynamic Broadcast Receiver, rather than Static Broadcast Receiver.
        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
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);
    }
}

接下来将介绍用于将广播消息发送到公共广播接收器的示例代码。在将广播发送给公共广播接收器时,必须注意恶意软件可以接收到广播消息。

要点(发送广播讯息):

  1. 请勿发送敏感信息。
  2. 在接收结果时,请小心、安全地处理结果数据。
PublicSenderActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.内部广播接收器—接收/发送广播

内部广播接收器从不接收来自内部应用程序之外的任何广播。它由多个内部应用程序组成,用于保护内部应用程序处理的信息或功能。

要点(接收广播信息):

  1. 定义接收广播消息的内部签名权限。
  2. 声明使用内部签名权限接收结果。
  3. 将导出的属性显式设置为 true。
  4. 需要静态广播接收器定义的内部签名权限。
  5. 需要内部签名权限才能注册动态广播接收器。
  6. 验证内部签名权限是否由内部应用程序定义。
  7. 小心、安全地处理接收到的广播,即使是从内部应用程序发出的广播。
  8. 由于请求的应用程序是内部的,因此可以返回敏感信息。
  9. 导出 APK 时,使用与发送应用程序相同的开发人员密钥签署 APK。

内部接收器是内部广播接收器的示例代码,可用于静态广播接收器和动态广播接收器。

InhouseReceiver.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
}

静态广播接收器将在 AndroidManifest.xml 中定义。请注意,根据终端版本的不同,隐式广播意图的接收可能会受到限制,如表 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>

动态广播接收器通过调用程序中的 registerReceiver() 或 unregisterReceiver() 执行注册/取消注册。为了通过按钮操作执行注册/取消注册,将在 InhouseReceiverActivity 中排列此按钮。由于动态广播接收器实例的范围比 InhouseReceiverActivity 长,因此不能将其作为 InhouseReceiverActivity 的成员变量进行保留。因此,请将动态广播接收器实例保持为 DynamicReceiverService 的成员变量,然后从 InhouseReceiverActivity 启动/结束 DynamicReceiverService,以间接注册/取消注册动态广播接收器。

InhouseReceiverActivity.java
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
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);
        filter.setPriority(1);  // Prioritize Dynamic Broadcast Receiver, rather than Static Broadcast Receiver.

        // *** 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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 9 *** 导出 APK 时,使用与发送应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.2.2 使用与发送应用程序相同的开发人员密钥签署 APK

接下来将介绍用于将广播消息发送到内部广播接收器的示例代码。

要点(发送广播讯息):

  1. 定义接收结果的内部签名权限。
  2. 声明以使用内部签名权限接收广播消息。
  3. 验证内部签名权限是否由内部应用程序定义。
  4. 由于请求的应用程序是内部应用程序,因此可以返回敏感信息。
  5. 需要接收器的内部签名权限。
  6. 小心、安全地处理接收到的结果数据。
  7. 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。
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" >

    <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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 16 *** 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.2.3 使用与目标应用程序相同的开发人员密钥签署 APK

4.2.2.规则手册

按照以下规则发送或接收广播消息。

  1. 仅在应用程序中使用的广播接收器必须设置为“私有”(必需)
  2. 小心、安全地处理接收到的意图(必需)
  3. 在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)
  4. 返回结果信息时,请注意目标应用程序中的结果信息是否泄露(必需)
  5. 使用广播发送敏感信息时,请限制允许的接收器(必需)
  6. 敏感信息不得包含在粘性广播中(必需)
  7. 请注意,未指定 receiverPermission 的有序广播可能无法传送(必需)
  8. 小心、安全地处理从广播接收器返回的结果数据(必需)
  9. 转手提供资产时,应使用相同的保护级别保护资产(必需)

4.2.2.1.仅在应用程序中使用的广播接收器必须设置为“私有”(必需)

应将仅在应用程序中使用的广播接收器设置为“私有”,以避免从其他应用程序意外收到任何广播。它将防止应用程序功能滥用或异常行为。

仅在同一应用程序中使用的接收器不应设计为使用意图过滤器。由于意图过滤器的特性,即使调用同一应用程序中的私有接收器,通过意图过滤器也可能意外调用其他应用程序的公共接收器。

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

请参阅“4.2.3.1.已导出属性和意图过滤器设置的组合(用于接收器)”。

4.2.2.2.小心、安全处理接收到的意图(必需)

虽然根据广播接收器的类型不同,风险也会有所不同,但在处理收到的意图数据时首先要验证意图的安全性。

由于公共广播接收器可从未指定的大量应用程序接收意图,因此它可能会接收恶意软件攻击意图。私有广播接收器从不直接从其它应用程序接收任何意图,但从其它应用程序接收到的公共组件的意图数据可能会被转发到私有广播接收器。因此,不要认为收到的意图是完全安全的而不进行验证。内部广播接收器具有一定程度的风险,因此它还需要验证接收到的意图的安全性。

请参阅“3.2.谨慎且安全地处理输入数据

4.2.2.3.在验证签名权限是否是由内部应用程序定义后,再使用内部定义的签名权限(必需)

内部广播接收器仅接收内部应用程序发送的广播,应通过内部定义的签名权限进行保护。AndroidManifest.xml 中的权限定义/权限请求声明不足以保护,因此请参阅“5.2.1.2.如何使用内部定义的签名权限在内部应用程序之间通信”,通过指定内部定义的签名权限来结束广播,receiverPermission 参数要以相同的方式进行验证。

4.2.2.4.返回结果信息时,请注意目标应用程序中的结果信息是否泄露(必需)

根据广播接收器的类型,通过 setResult() 返回结果信息的应用程序的可靠性会有所不同。对于公共广播接收器,目标应用程序可能是恶意软件,并且可能存在恶意使用结果信息的风险。对于私有广播接收器和内部广播接收器,结果目标是内部开发的应用程序,因此无需考虑结果信息处理。

当从以上广播接收器返回结果信息时,需要注意目标应用程序中的结果信息是否泄漏。

4.2.2.5.使用广播发送敏感信息时,请限制允许的接收器(必需)

广播是为将信息广播给未指定的大量应用程序,或一次性通知它们时间安排而创建的系统。因此,广播敏感信息需要仔细设计,以防止恶意软件非法泄露信息。

对于广播敏感信息,只有可靠的广播接收器可以接收信息,而其它广播接收器则不能接收。以下是广播发送方法的一些示例。

  • 此方法是通过广播发送来修复地址,并且只将广播信息发送给可靠的广播接收器。此方法中有 2 种模式。
    • 当它发送给同一应用程序中的广播接收器时,请按 Intent#setClass(Context, Class) 指定地址。请参阅示例代码部分“4.2.1.1.私有广播接收器 - 接收/发送广播消息”了解具体代码。
    • 当它被发送到其它应用程序中的广播接收器时,请按 Intent#setClassName(String, String) 指定地址。通过将目标数据包中 APK 签名的开发人员密钥与要发送广播信息的白名单进行比较,确认是否是允许的应用程序。实际上,以下隐式意图使用方法更实用。
  • 方法是通过指定内部定义的“签名权限”到 receiverPermission 参数来发送广播信息,并为可靠的广播接收器声明使用此签名权限。请参阅示例代码部分“4.2.1.3.内部广播接收器 - 接收/发送广播”了解具体代码。此外,实施此广播发送方法需要应用规则“4.2.2.3.在验证了签名权限是否由内部应用程序定义后,再使用内部定义的签名权限(必需)”。

4.2.2.6.敏感信息不得包含在粘性广播中(必需)

通常,广播信息在被可用的广播接收器接收后将消失。另一方面,即使被可用的广播接收器接收到,粘性广播(包括粘性有序广播)也不会从系统中消失,并且可由 registerReceiver 接收。当不需要粘性广播时,可以使用 removeStickyBroadcast() 随时删除它。

在其设计中,它假定隐式意图使用了粘性广播。无法发送具有指定 receiverPermission 参数的广播。因此,通过粘性广播发送的信息可由多个未指定的应用程序访问,包括恶意软件,因此不能以这种方式发送敏感信息。请注意,粘性广播在 Android 5.0(API 等级 21)中已弃用。

4.2.2.7.请注意,未指定 receiverPermission 的有序广播可能无法传送(必需)

未指定 receiverPermission 参数的有序广播可由包括恶意软件在内的大量未指定应用程序接收。有序广播用于接收来自接收器的返回信息,并使多个接收器逐个执行处理。广播讯息按优先级顺序发送至接收器。因此,如果高优先级恶意软件首先接收广播并执行 abortBroadcast(),广播将不会发送到下面的接收器。

4.2.2.8.小心、安全地处理从广播接收器返回的结果数据(必需)

基本上,考虑到接收的结果可能是攻击数据,应安全处理结果数据,但根据返回结果数据的广播接收器类型的不同,这些风险也会有所不同。

当发送方(源)广播接收器为公共广播接收器时,它将从大量未指定的应用程序接收返回的数据。因此,它还可能接收恶意软件的攻击数据。当发送方(源)广播接收器为私有广播接收器时,似乎没有风险。但是,其他应用程序接收的数据可能会作为结果数据间接转发。因此,如果没有任何鉴定,结果数据就不应被视为安全数据。当发送方(源)广播接收器为内部广播接收器时,存在一定程度的风险。因此,考虑到结果数据可能是攻击数据,应以安全的方式对其进行处理。

请参阅“3.2.谨慎且安全地处理输入数据

4.2.2.9.第二次提供资产时,应使用相同的保护级别保护资产(必需)

如果临时向其他应用程序提供受权限保护的信息或功能资产,则必须通过申请目标应用程序的相同权限来保持保护标准。在 Android 权限安全模式中,仅管理应用程序对受保护资产的直接访问权限。由于其特性,所获得的资产可提供给其他应用程序,而无需申请保护所必需的权限。这实际上与重新委派权限相同,因为它被称为权限重新委派问题。请参阅“5.2.3.4.权限重新委派问题。”

4.2.3.高级主题

4.2.3.1.已导出的属性和意图过滤器设置的组合(用于接收器)

表 4.2.3 显示了实施接收器时允许的导出设置和意图过滤器元素组合。下面介绍了禁止使用意图过滤器定义 exported=”false” 的主要原因。

表 4.2.3 是否可用;已导出属性和意图过滤器元素的组合
  导出属性的值
True False 未指定
已定义意图过滤器 OK (请勿使用) (请勿使用)
未定义意图过滤器 OK OK (请勿使用)

如果未指定接收器的导出属性,则接收器是否为公共接收器取决于是否存在该接收器的意图筛选器。[10] 但是,在此指南中,禁止将导出的属性设置为未指定。通常,如之前所述,最好避免使用依赖于任何给定 API 的默认行为的实施;此外,如果存在用于配置重要安全相关设置——如导出的属性——的显式方法,则最好始终使用这些方法。

[10]如果定义了任何意图过滤器,则接收器是公共的;否则是私有的。有关详细信息,请参阅 https://developer.android.com/guide/topics/manifest/receiver-element.html#exported。

即使将广播发送到同一应用程序内的私有接收器,也可能意外调用其它应用程序中的公共接收器。这就是禁止使用意图过滤器定义指定 exported="false" 的原因。以下 2 张图显示了意外调用的发生。

图 4.2.4 是一个正常行为示例,在同一应用程序中,隐式意图只能调用私有接收器(应用程序 A)。意图过滤器(图中的 action="X")仅在应用程序 A 中定义,因此这是预期行为。

_images/image41.png

图 4.2.4 正常行为示例

图 4.2.5 是在应用程序 B 和应用程序 A 中定义意图过滤器(参见图中的 action="X")的示例。首先,当另一个应用程序(应用程序 C)通过隐式意图发送广播时,私有接收者 (A-1) 不会接收它们。因此不会出现任何安全问题。(请参阅图中的橙色箭头标记。)

从安全的角度来看,问题在于应用程序 A 调用同一应用程序中的私有接收器。当应用程序 A 广播隐式意图时,不仅同一应用程序中的私有接收器,而且具有相同意图过滤器定义的公共接收器 (B-1) 也可以接收意图。(图中的红色箭头标记)。在这种情况下,敏感信息可能从应用程序 A 发送到 B。当应用程序 B 是恶意软件时,将导致敏感信息的泄露。当 广播为有序广播时,它可能会收到意外的结果信息。

_images/image42.png

图 4.2.5 异常行为的示例

但是,如果实施了广播接收器仅接收系统发送的广播意图,则应使用带有意图过滤器定义的 exported=”false”。不应使用其他组合。这是基于这样一个事实:系统发送的广播意图可以通过 exported="false" 来接收。如果其它应用程序发送意图,并且此意图与系统发送的广播意图具有相同的操作,则它可能会通过接收该意图而导致意外行为。但是,可以通过指定 exported="false" 来防止这种情况。

4.2.3.2.在启动应用程序之前不能注册接收器

请务必注意,在 AndroidManifest.xml 中静态定义的广播接收器在安装时不会自动启用。[11] 应用程序仅在首次启动广播后才能接收广播;因此,在作为触发器来启动操作安装后,无法使用广播接收信息。但是,如果在发送广播时设置 Intent.FLAG_INCLUDE_STOPPED_PACKAGES 标记,则即使尚未首次启动的应用程序也会接收到该广播。

[11]在 Android 3.0 之前的版本中,仅需安装应用程序即可自动注册接收器。

4.2.3.3.私有广播接收器可以接收由同一 UID 应用程序发送的广播

可以为多个应用程序提供相同的 UID。即使是私有广播接收器,也可以接收来自同一 UID 应用程序的广播。

但是,这不会成为安全问题。只要保证具有相同 UID 的应用程序具有一致的开发人员密钥来签署 APK。这意味着私有广播接收器仅接收来自内部应用程序的广播。

4.2.3.4.广播的类型和功能

关于广播消息,基于其是否是有序的,以及是否是粘滞型,有 4 种类型。根据广播发送方法,也可以确定要发送的广播类型。请注意,粘性广播在 Android 5.0(API 等级 21)中已弃用。

表 4.2.4 发送广播的类型
广播类型 发送方法 有序? 粘滞?
普通广播 sendBroadcast()
有序广播 sendOrderedBroadcast()
粘性广播 sendStickyBroadcast()
粘性有序广播 sendStickyOrderedBroadcast()

我们介绍了每种广播的功能。

表 4.2.5 每种广播的功能
广播类型 每种广播类型的功能
普通广播 普通广播在发送至应收广播接收器时会消失。多个广播接收器同时接收广播。这与有序广播不同。特定广播接收器可以接收广播信息。
有序广播 有序广播具有通过可接收的广播接收器逐个接收广播信息的特点。优先级较高的广播接收器将提前接收。广播信息将在发送到所有广播接收器或正在进行的广播接收器调用 abortBroadcast() 时消失。声明指定权限的广播接收器可以接收广播消息。此外,广播接收器发送的结果信息可以被带有有序广播的发送者接收。SMS 接收通知广播 (SMS_RECEIVED) 是有序广播的典型示例。
粘性广播 粘性广播不会消失并保留在系统中,然后调用 registerReceiver() 的应用程序稍后会收到粘性广播。由于粘滞广播与其它广播不同,因此它永远不会自动消失。因此,如果不需要粘性广播,则必须显式调用 removeStickyBroadcast() 才能删除粘性广播。此外,具有特定权限的受限广播接收器无法接收广播信息。不断变化的电池状态通知广播 (ACTION_BATTERY_CHANGED) 是粘滞广播的典型示例。
粘性有序广播 这是具有有序广播和粘滞广播两个特征的广播。与粘性广播相同,它不能只允许具有特定权限的广播接收器接收广播。

从广播特性行为视图中,上表相反地排列在以下视图中。

表 4.2.6 广播的特性行为
广播的特性行为 普通广播 有序广播 粘性广播 粘性有序广播
按权限限制可以接收广播信息的广播接收器 OK OK - -
从 Broadcast Receiver 获取过程结果 - OK - OK
使广播接收者按顺序处理广播 - OK - OK
稍后接收已发送的广播消息 - - OK OK

4.2.3.5.已广播的信息可能输出到 LogCat

基本上,发送/接收广播信息不会输出到 LogCat。但是,如果缺少权限导致接收器/发送器端出错,则会输出错误日志。广播发送的意图信息包括在错误日志中,因此,发生错误后,必须注意发送广播时,意图信息会显示在 LogCat 中。

发送器端缺少权限的错误

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

接收器端缺少许可的错误

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.在主屏幕上放置应用程序快捷方式时要注意的事项

下面我们讨论了在创建用于从主屏幕启动应用程序的快捷方式按钮或创建 Web 浏览器中的书签等 URL 快捷方式时要牢记的一些项目。作为示例,我们将考虑如下所示的实施。

在主屏幕上放置应用程序快捷方式

        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);

在上述代码片段发送的广播中,接收器是主屏幕应用程序,很难识别数据包名称;必须注意,这是一个以隐式意图传输到公共接收器的传输。因此,任何应用程序,包括恶意软件都可以接收此片段发送的广播;因此,在意图中包含敏感信息可能会造成信息泄露。特别需要注意的是,在创建基于 URL 的快捷方式时,URL 本身中可能包含秘密信息。

作为对策,必须遵循“4.2.1.2.公共广播接收器 - 接收/发送广播消息”中列出的要点,并确保传输的意图不包含敏感信息。

4.3.创建/使用内容提供器

因为 ContentResolver 和 SQLiteDatabase 的接口非常相似,所以通常会误认为内容提供器与 SQLiteDatabase 密切相关。但是,实际上,内容提供器只是提供应用程序间数据共享的界面,因此需要注意它是否会干扰每个数据保存格式。要在内容提供器中保存数据,可以使用 SQLiteDatabase,也可以使用其他保存格式,例如 XML 文件格式。以下示例代码中不包括任何数据保存过程,因此请根据需要进行添加。

4.3.1.示例代码

使用内容提供器的风险和对策因内容提供器的使用方式而异。在本章节中,我们根据内容提供器的使用方式将其分为 5 种类型。您可以通过下面显示的图表了解您应该创建哪种类型的内容提供器。

表 4.3.1 内容提供器类型的定义
类型 定义
私有内容提供器 不能由其他应用程序使用的内容提供器,因此是最安全的内容提供器
公共内容提供器 可以由大量未指定的应用程序使用的内容提供器
合作伙伴内容提供器 可供受信任合作伙伴公司的特定应用程序使用的内容提供器。
内部内容提供商 只能由其他内部应用程序使用的内容提供器
临时许可内容提供器 基本上是私有内容提供器,但允许特定应用程序访问特定 URI。
_images/image43.png

图 4.3.1 决定内容提供器类型的流程图

4.3.1.1.创建/使用私有内容提供器

私有内容提供器是仅在单个应用程序中使用的内容提供器,也是最安全的内容提供器 [12]

[12]但是,内容提供器的非公开设置在 Android 2.2(API 等级 8)和以前版本中不起作用。

下面显示了如何实施私有内容提供器的示例代码。

要点(创建内容提供器):

  1. 将导出的属性显式设置为 false。
  2. 即使接收到的请求数据来自相同的应用程序,也要小心、安全地处理这些数据。
  3. 由于所有敏感信息在同一应用程序中发送和接收,因此可以发送这些信息。
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
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);
        }
    }
}

下面是一个使用私有内容提供器的活动示例。

要点(使用内容提供器):

  1. 由于目标提供器位于同一应用程序中,因此可以发送敏感信息。
  2. 即使接收到的结果数据来自同一应用程序,也要小心、安全地处理这些数据。
PrivateUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.创建/使用公共内容提供器

公共内容提供器是可以由大量未指定的应用程序使用的内容提供器。需要注意的是,由于未指定客户端,它可能会受到恶意软件的攻击和篡改。例如,保存的数据可能由 select() 获取,数据可能通过 update() 更改,或者通过 insert()/delete() 插入/删除伪数据。

此外,使用非 Android 操作系统提供的自定义公共内容提供器时,需要注意恶意软件可能会接收请求参数,该恶意软件伪装为自定义公共内容提供器,还可能发送攻击性的结果数据。Android 操作系统提供的联系人和 MediaStore 也是公共内容提供器,但恶意软件无法伪装成它们。

实施公共内容提供器的示例代码如下所示。

要点(创建内容提供器):

  1. 将导出的属性显式设置为 true。
  2. 小心、安全地处理接收到的请求数据。
  3. 返回结果时,请勿包含敏感信息。
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
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);
        }
    }
}

下面是一个使用公共内容提供器的活动示例。

要点(使用内容提供器):

  1. 请勿发送敏感信息。
  2. 在接收结果时,请小心、安全地处理结果数据。
PublicUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.创建/使用合作伙伴内容提供器

合作伙伴内容提供器是仅可由特定应用程序使用的内容提供器。该系统由合作伙伴公司的应用程序和内部应用程序组成,用于保护在合作伙伴应用程序和内部应用程序之间处理的信息和功能。

实施仅供合作伙伴使用的内容提供器的示例代码如下所示。

要点(创建内容提供器):

  1. 将导出的属性显式设置为 true。
  2. 验证请求应用程序的证书是否已在自己的白名单中注册。
  3. 即使接收到的结果数据来自合作伙伴应用程序,也要小心、安全地处理接收到的请求数据。
  4. 可以返回允许向合作伙伴应用程序公开的信息。
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
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);
        }
    }
}

下面是一个使用合作伙伴内容提供器的活动示例。

要点(使用内容提供器):

  1. 验证目标应用程序的证书是否已在自己的白名单中注册。
  2. 可以发送允许向合作伙伴应用程序公开的信息。
  3. 即使接收到的结果数据来自合作伙伴应用程序,也要小心、安全地处理这些数据。
PartnerUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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.创建/使用内部内容提供器

内部内容提供器是禁止在内部应用程序之外的应用程序使用的内容提供器。

下面显示了如何实施仅限内部使用的内容提供器的示例代码。

要点(创建内容提供器):

  1. 定义内部签名权限。
  2. 请求内部签名权限。
  3. 将导出的属性显式设置为 true。
  4. 验证内部签名权限是否由内部应用程序定义。
  5. 验证参数的安全性,即使它是内部专用应用程序的请求。
  6. 由于请求的应用程序是内部的,因此可以返回敏感信息。
  7. 导出 APK 时,使用与请求应用程序相同的开发人员密钥签署 APK。
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
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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 7 *** 导出 APK 时,使用与请求的应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.3.2 使用与请求的应用程序相同的开发人员密钥签署 APK

下面是使用内部专用内容提供器的活动示例。

要点(使用内容提供器):

  1. 声明使用内部签名权限。
  2. 验证内部签名权限是否由内部应用程序定义。0
  3. 验证目标应用程序是否是使用内部证书签署。
  4. 由于目标应用程序是内部应用程序,因此可以发送敏感信息。
  5. 即使接收到的结果数据来自内部应用程序,也要小心、安全地处理这些数据。
  6. 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 13 *** 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.3.3 使用与目标应用程序相同的开发人员密钥签署 APK

4.3.1.5.创建/使用临时许可内容提供器

临时许可内容提供器基本上是专用内容提供程序,但这允许特定应用程序访问特定 URI。通过向目标应用程序发送指定了特殊标志的 Intent,可以为这些应用程序提供临时访问权限。内容提供器方应用程序可以主动向其他应用程序授予访问权限,还可以被动地向请求临时访问权限的应用程序授予访问权限。

下面显示了如何实施临时许可内容提供器的示例代码。

要点(创建内容提供器):

  1. 将导出的属性显式设置为 false。
  2. 指定使用 grant-uri-permission 临时授予访问权限的路径。
  3. 即使接收到的请求数据来自临时被授予访问权限的应用程序,也要小心、安全地处理这些数据。
  4. 可以返回允许向临时访问应用程序公开的信息。
  5. 为授予临时访问的意图指定 URI。
  6. 为授予临时访问权限的意图指定访问权限。
  7. 向应用程序发送显式意图以授予临时访问权限。
  8. 将意图返回到请求临时访问权限的应用程序。
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
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
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
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();
    }
}

下面是临时许可内容提供器的示例。

要点(使用内容提供器):

  1. 请勿发送敏感信息。
  2. 在接收结果时,请小心、安全地处理结果数据。
TemporaryUserActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.规则手册

在实施或使用内容提供器时,请确保遵循以下规则。

  1. 仅在应用程序中使用的内容提供器必须设置为私有(必需)
  2. 小心、安全地处理接收到的请求参数(必需)
  3. 在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)
  4. 返回结果时,请注意目标应用程序中该结果信息泄漏的可能性(必需)
  5. 第二次提供资产时,应使用相同保护级别保护资产(必需)

用户端也应遵循以下规则。

  1. 小心、安全地处理从内容提供器返回的结果数据(必需)

4.3.2.1.仅在应用程序中使用的内容提供器必须设置为专用(必需)

仅在单个应用程序中使用的内容提供器不需要其他应用程序访问,开发人员通常不会考虑对内容提供器进行攻击的访问。内容提供器基本上是共享数据的系统,因此默认情况下将其作为公用处理。仅在单个应用程序中使用的内容提供器应明确设置为专用,并且应是专用内容提供器。在 Android 2.3.1(API 等级 9)或更高版本中,可通过在提供器元素中指定 android:exported="false" 将内容提供程序设置为专用。

AndroidManifest.xml
    <!-- *** POINT 1 *** Set false for the exported attribute explicitly.-->
    <provider
        android:name=".PrivateProvider"
        android:authorities="org.jssec.android.provider.privateprovider"
        android:exported="false" />

4.3.2.2.小心、安全地处理接收到的请求参数(必需)

根据内容提供器的类型,风险会有所不同,但在处理请求参数时,您首先应该做的是输入验证。

尽管内容提供器的每种方法都有应该接收 SQL 语句的组件参数的接口,但实际上它只需将任意字符串放在系统中,因此需要注意内容提供器端需要假设可能提供意外参数的情况。

由于公共内容提供器可以接收来自不受信任来源的请求,因此可能会受到恶意软件的攻击。另一方面,专用内容提供器将永远不会直接从其他应用程序收到任何请求,但目标应用程序中的公共活动可能会将恶意意图转发给专用内容提供器,因此您不应假定专用内容提供器无法接收任何恶意输入。

由于其他内容提供器也有被转发恶意意图的风险,因此也有必要对这些请求执行输入验证。

请参阅“3.2.谨慎且安全地处理输入数据

4.3.2.3.在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)

创建内容提供器时,请确保通过定义内部签名权限来保护您的内部内容提供器。由于在 AndroidManifest.xml 文件中定义权限或声明权限请求不能提供足够的安全性,请务必参阅“5.2.1.2.如何使用内部定义的签名权限在内部应用程序之间通信。”

4.3.2.4.返回结果时,请注意目标应用程序中该结果信息的泄漏可能性(必需)

如果是 query() 或 insert(),Cursor 或 Uri 将作为结果信息返回给发送请求的应用程序。如果结果信息中包含敏感信息,则信息可能会从目标应用程序中泄露。如果是 update() 或 delete(),更新/删除的记录数将作为结果信息返回给发送请求的应用程序。在极少数情况下,根据某些应用程序规格,更新/删除的记录数具有敏感含义,因此请注意这一点。

4.3.2.5.提供次级资产时,应使用相同保护级别保护资产(必需)

如果将受权限保护的信息或功能资产转手提供给另一个应用程序,您需要确保它具有访问该资产所需的相同权限。在 Android 操作系统权限安全模型中,只有获得适当权限的应用程序才能直接访问受保护的资产。但是,这存在漏洞,因为对资产具有权限的应用程序可以充当代理并允许访问无特权应用程序。实质上,这与重新委派权限相同,因此被称为“权限重新委派”问题。请参阅“5.2.3.4.权限重新委派问题。”

4.3.2.6.小心、安全地处理从内容提供器返回的结果数据(必需)

根据内容提供器的类型,风险会有所不同,但在处理结果数据时,您首先应该做的是输入验证。

如果目标内容提供器是公共内容提供器,伪装为公共内容提供器的恶意软件可能会返回攻击性的结果数据。另一方面,如果目标内容提供器是专用内容提供器,则其风险较小,因为它从同一应用程序接收结果数据,但您不应假定专用内容提供器无法接收任何恶意输入。由于其他内容提供器也有被返回恶意数据的风险,因此也有必要对这些结果数据执行输入验证。

请参阅“3.2.谨慎且安全地处理输入数据

4.4.创建/使用服务

4.4.1.示例代码

使用服务的风险和对策因服务的使用方式而异。您可以通过下面的图表了解应该创建的服务类型。由于安全编码的最佳做法因创建服务的方式而异,因此我们还将解释服务的实施。

表 4.4.1 服务类型的定义
类型 定义
专用服务 不能使用其他应用程序的服务,因此是最安全的服务。
公共服务 可以由大量未指定应用程序使用的服务
合作伙伴服务 一种只能由受信任的合作伙伴公司提供的特定应用程序使用的服务。
内部服务 只能由其他内部应用程序使用的服务。
_images/image44.png

图 4.4.1 选择服务类型的流程图

服务有多种实施方法,您需要选择与您要创建的服务类型匹配的方法。表中垂直列的项目显示了实施方法,这些方法共分为 5 种类型。“确定”表示可能的组合,而其他符号则表示表中不可能/困难的组合。

请参阅“4.4.3.2.如何实施服务”和每种服务类型的示例代码(表中带有 * 标记的代码),以了解详细的服务实施方法。

表 4.4.2 服务的实施方法
类别 专用服务 公共服务 合作伙伴服务 内部服务
startService 类型 确定* OK - OK
IntentService 类型 OK 确定* - OK
本地绑定类型 OK - - -
Messenger 绑定类型 OK OK - 确定*
AIDL 绑定类型 OK OK 确定* OK

通过组合使用表 4.4.2 中的 * 标记,下面显示了每种服务安全类型的示例代码。

4.4.1.1.创建/使用专用服务

专用服务是其他应用程序无法启动的服务,因此它是最安全的服务。

当使用仅在应用程序内使用的“专用服务”时,只要您对分类使用显式意图,就不必担心意外将其发送到任何其他应用程序。

如何使用 startService 类型服务的示例代码如下所示。

要点(创建服务):

  1. 将导出的属性显式设置为 false。
  2. 即使是从同一应用程序发送的意图,也应小心、安全地处理收到的意图。
  3. 由于请求应用程序位于同一应用程序中,因此可以发送敏感信息。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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;
    }
}

接下来是使用专用服务的活动的示例代码。

要点(使用服务):

  1. 在指定类中使用显式意图,以调用同一应用程序中的服务。
  2. 由于目标服务位于同一应用程序中,因此可以发送敏感信息。
  3. 即使接收到的结果数据来自同一应用程序中的服务,也要小心、安全地处理这些数据。
PrivateUserActivity.java
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);
    }

    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.创建/使用公共服务

公共服务是指可以被大量未指定的应用程序使用的服务。需要注意的是,它可能会收到恶意软件发送的信息(意图等)。在使用公共服务的情况下,必须注意可能会收到恶意软件发送的信息(意图等)。

如何使用 startService 类型服务的示例代码如下所示。

要点(创建服务):

  1. 将导出的属性显式设置为 true。
  2. 小心、安全地处理收到的意图。
  3. 返回结果时,请勿包含敏感信息。
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" >

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:allowBackup="false" >
              
        <!-- Most standard Service -->
        <!-- *** POINT 1 *** Explicitly set the exported attribute to true.-->
        <service android:name=".PublicStartService" android:exported="true">
            <intent-filter>
                <action android:name="org.jssec.android.service.publicservice.action.startservice" />
            </intent-filter>
        </service>
        
        <!-- Public Service derived from IntentService class -->
        <!-- *** POINT 1 *** Explicitly set the exported attribute to true.-->
        <service android:name=".PublicIntentService" android:exported="true">
            <intent-filter>
                <action android:name="org.jssec.android.service.publicservice.action.intentservice" />
            </intent-filter>
        </service>

        </application>

</manifest>
PublicIntentService.java
package org.jssec.android.service.publicservice;

import android.app.IntentService;
import android.content.Intent;
import android.widget.Toast;

public class PublicIntentService extends IntentService{

    /**
     * Default constructor must be provided when a service extends IntentService class.
     * 如果不存在,则会出现错误。
     */
    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) {        
        // *** 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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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();
    }
    
}

接下来是使用公共服务的活动的示例代码。

要点(使用服务):

  1. 请勿发送敏感信息。
  2. 在接收结果时,请小心、安全地处理结果数据。
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" >

    <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
package org.jssec.android.service.publicserviceuser;

import android.app.Activity;
import android.content.Intent;
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");

        startService(intent);
        // *** POINT 6 *** When receiving a result, handle the result data carefully and securely.
        // This sample code uses startService(), so receiving no result.
    }
    

    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");

        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.创建/使用合作伙伴服务

合作伙伴服务是只能由特定应用程序使用的服务。系统由合作伙伴公司的应用程序和内部应用程序组成,用于保护在合作伙伴应用程序和内部应用程序之间处理的信息和功能。

下面是 AIDL 绑定类型服务的一个示例。

要点(创建服务):

  1. 不要定义意图筛选器,并将导出的属性显式设置为 true。
  2. 验证请求应用程序的证书是否已在自己的白名单中注册。
  3. 通过 onBind (onStartCommand, onHandleIntent),不(无法)识别请求的应用程序是否为合作伙伴应用程序。
  4. 即使是从合作伙伴应用程序发出的意图,也应小心、安全地处理接收到的意图。
  5. 仅返回允许向合作伙伴应用程序公开的信息。

此外,请参阅“5.2.1.3. 如何验证应用程序证书的哈希值”,以了解如何验证指定给白名单的目标应用程序的证书哈希值。

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 *** Do not define the intent filter and explicitly set the exported attribute to true.-->
       <service 
           android:name="org.jssec.android.service.partnerservice.aidl.PartnerAIDLService" 
           android:exported="true" />
    </application>

</manifest>

在本示例中,将创建 2 个 AIDL 文件。一种是回调接口,用于将数据从“服务”提供给“活动”。另一个接口用于将数据从活动提供给服务并获取信息。此外,AIDL 文件中描述的程序包名称应与创建 AIDL 文件的目录层次结构保持一致,与 java 文件中描述的程序包名称相同。

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
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
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

接下来是仅使用合作伙伴服务的活动的示例代码。

要点(使用服务):

  1. 验证目标应用程序的证书是否已在自己的白名单中注册。
  2. 仅返回允许向合作伙伴应用程序公开的信息。
  3. 使用显式意图调用合作伙伴服务。
  4. 即使接收到的结果数据来自合作伙伴应用程序,也要小心、安全地处理这些数据。
PartnerAIDLUserActivity.java
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.通知服务。
    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();
    }
    
    public void onGetInfoClick(View v) {
        getServiceinfo();
    }
    
    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.请参阅“3.2 谨慎且安全地处理输入数据”。
            Toast.makeText(mContext, String.format("Received \"%s\" from service.", info), Toast.LENGTH_SHORT).show();
         }
    }
}
PkgCertWhitelists.java
package org.jssec.android.shared;

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, it is possible to validate directly by new API of PackageManager
            PackageManager pm = ctx.getPackageManager();
            return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
        } else {
            // else (API Level < 28) use a facility of PkgCert
            return PkgCert.test(ctx, pkgname, correctHash);
	}
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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.创建/使用内部服务

内部服务是指禁止在内部应用程序之外的应用程序使用的服务。它们用于内部开发的、需要安全地共享信息和功能的应用程序。

下面是使用 Messenger 绑定类型服务的一个示例。

要点(创建服务):

  1. 定义内部签名权限。
  2. 请求内部签名权限。
  3. 不要定义意图筛选器,并将导出的属性显式设置为 true。
  4. 验证内部签名权限是否由内部应用程序定义。
  5. 小心、安全地处理接收到的意图,即使是从内部应用程序发出的意图。
  6. 由于请求的应用程序是内部的,因此可以返回敏感信息。
  7. 导出 APK 时,使用与请求应用程序相同的开发人员密钥签署 APK。
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 *** Do not define the intent filter and explicitly set the exported attribute to true.-->
        <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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
        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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 7 *** 导出 APK 时,使用与请求的应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.4.2 使用与请求的应用程序相同的开发人员密钥签署 APK

接下来是仅使用内部服务的活动的示例代码。

要点(使用服务):

  1. 声明使用内部签名权限。
  2. 验证内部签名权限是否由内部应用程序定义。
  3. 验证目标应用程序是否是使用内部证书签署。
  4. 由于目标应用程序在内部,因此可以返回敏感信息。
  5. 使用显式意图调用内部服务。
  6. 即使接收到的结果数据来自内部应用程序,也要小心、安全地处理这些数据。
  7. 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。
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" >

    <!-- *** 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
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();
    }

    public void onGetInfoClick(View v) {
        getServiceinfo();
    }

    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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 14 *** 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.4.3 使用与目标应用程序相同的开发人员密钥签署 APK

4.4.2.规则手册

实施或使用服务时,请遵循以下规则。

  1. 仅在应用程序中使用的服务必须设置为私有(必需)
  2. 小心、安全地处理接收到的数据(必需)
  3. 在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)
  4. 请勿在 onCreate 中确定服务是否提供其功能(必需)
  5. 返回结果信息时,请注意目标应用程序中的结果信息是否泄漏(必需)
  6. 如果目标服务是固定的,请使用显式意图(必需)
  7. 如果链接到其他公司的应用程序,请验证目标服务(必需)
  8. 第二次提供资产时,应使用相同级别的保护措施保护资产(必需)
  9. 尽量少发送敏感信息(推荐)

4.4.2.1.仅在一个应用程序中使用的服务必须设置为专用(必需)

仅在一个应用程序(或同一 UID)中使用的服务必须设置为“专用”。它可避免应用程序意外地从其他应用程序接收意图,并最终防止应用程序功能损坏或应用程序行为异常。

在 AndroidManifest.xml 中定义服务时,您在实施中必须执行的所有操作是将导出属性设置为 false。

AndroidManifest.xml
    <!-- Private Service derived from Service class -->
    <!-- *** POINT 1 *** Set false for the exported attribute explicitly.-->
    <service android:name=".PrivateStartService" android:exported="false"/>

此外,还有一种罕见的情况,如果服务仅在应用程序中使用,则不用设置意图过滤器。原因是,由于意图筛选器的特性,尽管您打算在应用程序中调用专用服务,也可能会意外调用其他应用程序中的公共服务。

AndroidManifest.xml(Not recommended)
    <!-- Private Service derived from Service class -->
    <!-- *** POINT 1 *** Set false for the exported attribute explicitly.-->
    <service android:name=".PrivateStartService" android:exported="false">
        <intent-filter>
            <action android:name=”org.jssec.android.service.OPEN />
        </intent-filter>
    </service>

请参阅“4.4.3.1.已导出属性和意图过滤器设置的组合(在服务案例中)”。

4.4.2.2.小心、安全地处理接收到的数据(必需)

与“活动”一样,如果是“服务”,则在处理收到的意图数据时,您首先应该做的是输入验证。此外,在服务用户端,必须验证来自服务的结果信息的安全性。请参阅“4.1.2.5.小心、安全处理已接收的意图(必需)”和“4.1.2.9.小心、安全地处理请求的活动返回的数据(必需)。”

在“服务”中,您还应实施调用方法并小心地按“消息”交换数据。

请参阅“3.2.谨慎且安全地处理输入数据

4.4.2.3.在验证内部应用程序是否定义了内部定义的签名权限后再使用它(必需)

创建服务时,请确保通过定义内部签名权限来保护内部服务。由于在 AndroidManifest.xml 文件中定义权限或声明权限请求不能提供足够的安全性,请务必参阅“5.2.1.2.如何使用内部定义的签名权限在内部应用程序之间通信。”

4.4.2.4.请勿在 onCreate 中确定服务是否提供其功能(必需)

onCreate 中不应包括意图参数验证或内部定义的签名权限验证等安全检查,因为在服务运行期间接收新请求时,不会执行 onCreate 过程。因此,在实施由 startService 启动的服务时,应通过 onStartCommand 执行判断(如果使用 IntentService,则应通过 onHandleIntent 执行判断。)在实施由 bindService 启动的服务时,也是如此,应由 onBind 执行判断。

4.4.2.5.返回结果信息时,请注意目标应用程序中的结果信息是否泄漏(必需)

根据服务类型,结果信息目标应用程序(回调接收器端/消息目标)的可靠性会有所不同。考虑到目标可能是恶意软件,因此需要认真考虑信息泄露的可能性。

请参阅活动“4.1.2.7.返回结果时,请注意目标应用程序中该结果信息泄漏的可能性(必需)”,了解更多详情。

4.4.2.6.如果目标服务是固定的,请使用显式意图(必需)

使用隐式意图提供的服务时,如果意图过滤器的定义相同,则意图将发送至之前安装的服务。如果之前有意安装了具有相同意图过滤器的恶意软件,则意图将会被发送到恶意软件并发生信息泄露。另一方面,当使用显式意图提供的服务时,只有目标服务会收到意图,这样会更安全。

还有其他一些要点需要考虑,请参阅“4.1.2.8.如果预定了目标活动,请使用显式意图。(必需)。”

4.4.2.7.如果链接到其他公司的应用程序,请验证目标服务(必需)

在链接到其他公司的应用程序时,请确保有白名单。为此,您可以在应用程序中保存公司证书哈希的副本,并使用目标应用程序的证书哈希进行检查。这将防止恶意应用程序冒充发送 Intent。请参阅示例代码部分“4.4.1.3.创建/使用合作伙伴服务”,了解具体的实施方法。

4.4.2.8.第二次提供资产时,应使用相同级别的保护措施保护资产(必需)

如果将受权限保护的信息或功能资产转手提供给另一个应用程序,您需要确保它具有访问该资产所需的相同权限。在 Android 操作系统权限安全模型中,只有获得适当权限的应用程序才能直接访问受保护的资产。但是,这存在漏洞,因为对资产具有权限的应用程序可以充当代理并允许访问无特权应用程序。实质上,这与重新委派权限相同,因此被称为“权限重新委派”问题。请参阅“5.2.3.4.权限重新委派问题。”

4.4.2.9.尽量少发送敏感信息(推荐)

您不应将敏感信息发送给不受信任方。

在与服务交换敏感信息时,您需要考虑信息泄露的风险。您必须假定恶意第三方可以获取意图中发送给公共服务的所有数据。此外,在将意向发送给合作伙伴或内部服务时,也会存在各种信息泄露风险,具体取决于实施情况。

一开始就不发送敏感数据是防止信息泄露的唯一完美解决方案,因此您应尽可能限制发送的敏感信息量。当需要发送敏感信息时,最佳做法是仅发送给受信任的服务,并确保信息不会通过 LogCat 泄露。

4.4.3.高级主题

4.4.3.1.已导出属性和意图过滤器设置的组合(在服务案例中)

我们已在本指南中说明了如何实施这四种类型的服务:专用服务、公共服务、合作伙伴服务和内部服务。对于在 AndroidManifest.xml 文件中定义的导出属性的每种类型和在下表中定义的意图过滤器元素,有各种允许的设置组合。请验证导出的属性和意图过滤器元素与您尝试创建的服务的兼容性。

表 4.4.3 已导出属性和意图过滤器设置的组合
  导出属性的值
true false 未指定
已定义意图过滤器 公共 (请勿使用) (请勿使用)
未定义意图过滤器 公共、合作伙伴、内部 私有 (请勿使用)

如果在服务中未指定导出的属性,则是否定义意图筛选器将决定服务是否为公共服务;[13] 但是,在此指南手册中,禁止将服务的导出属性设置为未指定。通常,如之前所述,最好避免使用依赖于任何给定 API 的默认行为的实施;此外,如果存在用于配置重要安全相关设置(如导出的属性)的显式方法,,则最好始终使用这些方法。

[13]如果定义了任何意图过滤器,则服务为公共;否则为专用。更多信息,请参阅 https://developer.android.com/guide/topics/manifest/service-element.html#exported

不应使用未定义的意图过滤器和导出的 false 属性的原因是 Android 行为中存在漏洞,并且由于意图过滤器的工作方式,可能会意外调用其他应用程序的服务。

具体来说,Android 的行为如下所述,因此在设计应用程序时必须仔细考虑。

  • 当多个服务定义相同的意图过滤器内容时,将优先考虑之前安装的应用程序中的服务定义。
  • 如果使用了显式意图,则操作系统会自动选择并调用优先服务。

下面的三个图描述了由于 Android 行为而发生意外调用的系统。图 4.4.4 是仅可从同一应用程序中通过隐式意图调用专用服务(应用程序 A)的正常行为示例。由于只有应用程序 A 定义意图过滤器(图中的 action="X"),因此它运行正常。这是正常行为。

_images/image45.png

图 4.4.4 正常行为示例

图 4.4.5图 4.4.6 显示了在应用程序 B 和应用程序 A 中定义相同意图过滤器 (action=”X”) 的情形。

图 4.4.5 显示应用程序按顺序安装的情形,即应用程序 A -> 应用程序 B。在这种情况下,当应用程序 C 发送隐式意图时,调用专用服务 (A-1) 失败。另一方面,由于应用程序 A 可以按预期的隐式意图成功调用应用程序内的专用服务,因此在安全性方面不会出现任何问题(恶意软件的计数器测量)。

_images/image46.png

图 4.4.5 应用程序按应用程序 A -> 应用程序 B 的顺序安装

图 4.4.6 显示了应用程序按 applicationB -> applicationA 的顺序安装的情形。在这里,安全性方面存在问题。它显示了一个示例,应用程序 A 尝试通过发送隐式意图调用应用程序中的专用服务,但实际上调用了之前安装的应用程序 B 中的公共活动 (B-1)。由于存在此漏洞,可能会将敏感信息从应用程序 A 发送到应用程序 B。如果应用程序 B 为恶意软件,则会导致敏感信息泄露。

_images/image47.png

图 4.4.6 按应用程序 B -> 应用程序 A 的顺序安装应用程序

如上所示,使用意图过滤器将隐式意图发送至专用服务可能导致意外行为,因此最好避免此设置。

4.4.3.2.如何实施服务

由于服务实施的方法各不相同,因此应考虑根据示例代码中提供的安全类型(对每种类型的特征都进行了简要说明)进行选择。可以大致分为使用 startService 和使用 bindService 的情况。此外,还可以创建可在 startService 和 bindService 中使用的服务。应调查以下事项以确定服务的实施方法。

  • 是否向其他应用程序披露服务(服务披露)
  • 是否在运行期间交换数据(相互发送/接收数据)
  • 是否控制服务(启动或完成)
  • 是否作为另一个流程执行(流程之间的通信)
  • 是否并行执行多个进程(并行进程)

表 4.4.4 显示了实施方法的类别和每个项目的可行性。

“NG”代表不可能的情况,或需要与所提供功能不同的另一框架工作。

表 4.4.4 服务实施方法的类别
类别 服务披露 相互发送/接收数据 控制服务(启动/退出) 流程之间的通信 并行处理
startService 类型 OK NG OK OK NG
IntentService 类型 OK NG NG OK NG
本地绑定类型 NG OK OK NG NG
Messenger 绑定类型 OK OK OK OK NG
AIDL 绑定类型 OK OK OK OK OK
startService 类型

这是最基本的服务。它继承服务类别,并由 onStartCommand 执行进程。

在用户端,按意图指定服务,然后由 startService 调用。由于无法将结果等数据直接返回至意图来源,因此应将其与广播等其它方法结合使用。请参阅“4.4.1.1.创建/使用专用服务”,了解具体示例。

安全性检查应由 onStartCommand 执行,但不能用于仅合作伙伴服务,因为无法获取源的程序包名称。

IntentService 类型

IntentService 是通过继承服务创建的类。调用方法与 startService 类型相同。下面是与标准服务(startService 类型)相比具有的特性。

  • 处理意图由 onHandleIntent 完成(不使用 onStartCommand)。
  • 它由另一个线程执行。
  • 进程将排队。

调用将立即返回,因为进程由另一个线程执行,并且关于意图的进程将由排队系统连续执行。每个意图不会并行处理,但也可根据产品要求进行选择,作为简化实施的一个选项。由于无法将结果等数据返回至意图来源,因此应将其与广播等其它方法结合使用。请参阅“4.4.1.2.创建/使用公共服务”了解具体的实施示例。

安全性检查应由 onHandleIntent 执行,但不能用于仅合作伙伴服务,因为无法获取源的程序包名称。

本地绑定类型

这是一种实施本地服务的方法,该方法仅在与应用程序相同的流程中工作。定义派生自 Binder 类的类,并准备提供在服务中为调用方端实施的功能(方法)。

从用户端,使用 bindService 指定按意图列出的服务并调用服务。这是绑定服务的所有方法中最简单的实施方法,但由于它不能由另一个进程启动,而且不能披露服务,因此其用途有限。有关具体实施示例,请参阅示例代码中包含的项目“Service PrivateServiceLocalBind”。

从安全的角度来看,只能实施专用服务。

Messenger 绑定类型

这是使用 Messenger 系统实现与服务链接的方法。

由于 Messenger 可作为服务用户端的消息目标来提供,因此可以相对轻松地实现双向数据交换。此外,由于进程将排队,因此它具有一个特性,表现出“线程安全”。每个流程的并行流程是不可能的,但也可以根据产品的要求选择简化实施的选项。关于用户端,请按意图指定服务,然后使用 bindService 调用服务。请参阅“4.4.1.4.创建/使用内部服务”作为具体的实施示例。

必须在 onBind 中或通过 Message Handler 进行安全检查,但是,由于无法获取源的程序包名称,它不能用于仅限合作伙伴的服务。

AIDL 绑定类型

这是一种通过使用 AIDL 系统与服务链接的方法。定义 AIDL 的接口,并提供服务作为一种方法具有的功能。此外,还可以通过在用户端实施由 AIDL 定义的接口实现回调,可以进行多线程调用,但必须在服务端显式实施以进行排他过程。

用户端可以通过指定意图和使用 bindService 来调用服务。请参阅“4.4.1.3.创建/使用合作伙伴服务”了解具体的实施示例。

对于仅限内部使用的服务,必须在 onBind 中检查安全性;对于仅限合作伙伴使用的服务,必须通过由 AIDL 定义的每种接口方法进行检查。

这可用于本指南中所述的所有安全类型的服务。

4.5.使用 SQLite

之后,在使用 SQLite 创建/操作数据库时,在安全性方面有一些注意事项。要点是对数据库文件的访问权限和 SQL 注入的计数器测量进行适当设置。允许从外部直接读取/写入数据库文件的数据库(在多个应用程序之间共享)在此处不予考虑,但会考虑内容提供器后端和应用程序本身中的使用情况。此外,建议采取下面提到的应对措施,以防处理的敏感信息不多,但此处应该处理一定级别的敏感信息。

4.5.1.示例代码

4.5.1.1.创建/操作数据库

在 Android 应用程序中处理数据库时,可以使用 SQLiteOpenHelper 对数据库文件和访问权限设置(拒绝其他应用程序访问的设置)进行适当安排[14]。以下是一个简单应用程序示例,该应用程序在启动时创建数据库,并通过 UI 执行搜索/添加/更改/删除数据等操作。示例代码是为 SQL 注入执行的对策,以避免对外部的输入执行不正确的 SQL。

[14]就文件存储而言,可以将绝对文件路径指定为 SQLiteOpenHelper 构造函数的第二个参数(名称)。因此,如果指定了 SD 卡路径,则需要注意,其他应用程序可以读取和写入存储的文件。
_images/image48.png

图 4.5.1 在 Android 应用程序中使用数据库

要点:

  1. SQLiteOpenHelper 应该用于创建数据库。
  2. 使用占位符。
  3. 根据应用程序要求验证输入值。
SampleDbOpenHelper.java
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.写入流程,如数据转换。
    }

}
DataSearchTask.java(SQLite Database Project)
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.
        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("_", "@_"); //Escape _ in info which was received as input.
        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
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.规则手册

使用 SQLite 时,请遵循以下相应规则。

  1. 正确设置数据库文件的位置和访问权限(必需)
  2. 在与其他应用程序共享数据库数据时使用内容提供器进行访问控制(必需)
  3. 在数据库操作期间,必须在处理变量参数中使用占位符。(必需)

4.5.2.1.正确设置数据库文件位置和访问权限(必需)

考虑到数据库文件数据的保护,数据库文件位置和访问权限设置是需要一起考虑的非常重要的元素。

例如,即使正确设置了文件访问权限,如果其文件位置无法设置访问权限,如 SD 卡,则任何人都可以访问数据库文件。在位于应用程序目录中的情况下,如果未正确设置访问权限,最终将允许意外访问。以下是关于正确的位置分配和访问权限设置及其实现方法的一些要点。

关于位置和访问权限设置,考虑到保护数据库文件(数据),需要执行 2 个要点,如下所示。

  1. 位置

在可通过 Context#getDatabasePath(String name) 获取的文件路径中,或在某些情况下,可通过 Context#getFilesDir [15] 获取的目录中查找。

  1. 访问权限

设置为 MODE_PRIVATE(= 只能由创建文件的应用程序访问)模式。

[15]这两种方法都提供了(软件包)目录下的路径,该路径只能由指定的应用程序读取和写入。

通过执行以下 2 个要点,可以创建其他应用程序无法访问的数据库文件。下面是一些执行它们的方法。

1.使用 SQLiteOpenHelper

2.使用 Context#openOrCreateDatabase

创建数据库文件时,可以使用 SQLiteDatabase#openOrCreateDatabase。但是,使用此方法时,在某些 Android 智能手机设备中,将创建可从其他应用程序读取的数据库文件。因此,建议避免使用此方法,而是使用其他方法。上述两种方法的特征如下所示。

使用 SQLiteOpenHelper

使用 SQLiteOpenHelper 时,开发人员不必担心很多事情。创建从 SQLiteOpenHelper 派生的类,并将数据库名称(用于文件名)[16] 指定为构造器参数,然后将自动创建符合上述安全要求的数据库文件。

请参阅“4.5.1.1.创建/操作数据库”,以了解如何使用。

[16](在 Android 参考文件中未记录)由于在 SQLiteOpenHelper 实施中可以将完整的文件路径指定为数据库名称,因此需要注意不要在无意中指定没有访问控制功能(例如 SD 卡)的位置(路径)。
使用 Context#openOrCreateDatabase

使用 Context#openOrCreateDatabase 方法创建数据库时,应通过选项指定文件访问权限,在这种情况下,请明确指定 MODE_PRIVATE。

关于文件排列,指定数据库名称(将用于文件名)可以与 SQLiteOpenHelper 相同,在满足上述安全要求的文件路径中将自动创建一个文件。但是,也可以指定完整路径,因此在指定 SD 卡时,即使指定 MODE_PRIVATE,其他应用程序也可以访问。

显式执行数据库访问权限设置的示例: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
    }

有三种可能的访问权限设置:MODE_PRIVATE、MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE。这些常数可以由“OR”运算符一起指定。但是,MODE_PRIVATE 以外的所有设置在 API 等级 17 和更高版本中已弃用,并且将导致 API 等级 24 和更高版本中出现安全异常。即使是针对 API 等级 15 及更低版本的应用程序,通常最好不要使用这些标志。[17]

  • MODE_PRIVATE 只有创造者应用程序可读写
  • MODE_WORLD_READABLE 创造者应用程序可读写,其他应用程序只能读入
  • MODE_WORLD_WRITEABLE 创造者应用程序可以读写,其他应用程序只能写入
[17]有关 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 及其使用注意事项的详细信息,请参阅“4.6.3.2.目录的访问权限设置”。

4.5.2.2.在与其他应用程序共享数据库数据时使用内容提供器进行访问控制(必需)

与其他应用程序共享数据库数据的方法是将数据库文件创建为 WORLD_READABLE、WORLD_WRITEABLE,以便其他应用程序直接访问。但是,此方法不能限制对数据库的访问或对数据库的操作,因此数据可以由意外方(应用程序)读取或写入。因此,可以认为数据的机密性或一致性可能会出现某些问题,或者可能成为恶意软件的攻击目标。

如上所述,当与 Android 中的其他应用程序共享数据库数据时,强烈建议使用内容提供器。通过使用内容提供器,不仅可以实现对数据库的访问控制,从安全角度获得优势,还可以将数据库方案结构隐藏到内容提供器中,获得设计方面的优势。

4.5.2.3.在数据库操作期间,必须在处理变量参数中使用占位符。(必需)

在防止 SQL 注入方面,在将任意输入值合并到 SQL 语句时,应使用占位符。使用占位符执行 SQL 有两种方法,如下所示。

  1. 使用 SQLiteDatabase#compileStatement(),之后使用 SQLiteStatement#bindString() 或 bindLong() 将参数放置在占位符后面,获取 SQLiteStatement。
  2. 在 SQLiteDatabase 类中调用 execSQL(),、insert()、update()、delete()、query()、rawQuery() 和 replace() 时,使用包含占位符的 SQL 语句。

此外,在使用 SQLiteDatabase#compileStatement() 执行 SELECT 命令时,存在一个限制,即“由于 SELECT 命令,只能获取第 1 个元素”,因此使用受到限制。

在任一方法中,最好根据应用程序要求提前检查提供给占位符的数据内容。以下是每种方法的进一步说明。

使用 SQLiteDatabase#compileStatement():

在以下步骤中将数据提供给占位符。

  1. 使用 SQLiteDatabase#compileStatement()(作为 SQLiteStatement)获取包含占位符的 SQL 语句。
  2. 使用 bindLong() 和 bindString() 等方法,将创建的作为 SQLiteStatement 对象的目标设置为占位符。
  3. 通过 ExecSQLiteStatement 对象的 execute() 等方法执行 SQL。

占位符的使用情形:DataInsertTask.java(额外)

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

这是一种 SQL 语句,将在预先创建对象时执行,并为其分配参数。要执行的进程是固定的,因此没有空间进行 SQL 注入。此外,有一项优点是通过重新利用 SQLiteStatement 对象来提高进程效率。

为 SQLiteDatabase 提供的每个进程的使用方法

SQLiteDatabase 提供两种类型的数据库操作方法。一个是使用的 SQL 语句,另一个是不使用 SQL 语句。使用 SQL 语句的方法是 SQLiteDatabase#execSQL()/rawQuery(),可按照以下步骤执行。

  1. 准备包括占位符的 SQL 语句。
  2. 创建要分配到占位符的数据。
  3. 将 SQL 语句和数据作为参数发送,并为进程执行一种方法。

另一方面,SQLiteDatabase#insert()/update()/delete()/query()/replace() 是不使用 SQL 语句的方法。使用时,应按照以下步骤发送数据。

  1. 如果存在要插入/更新到数据库的数据,请注册到 ContentValues。
  2. 将 ContentValues 作为参数发送,然后为每个进程执行一个方法(在以下示例中为 SQLiteDatabase#insert())

每个进程的方法的使用情形 (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;
        }
    }

在此示例中,不直接编写 SQL 命令,而是使用 SQLiteDatabase 提供的插入方法。SQL 命令不直接使用,因此此方法中也没有 SQL 注入的空间。

4.5.3.高级主题

4.5.3.1.在 SQL 语句的 LIKE 谓词中使用通配符时,应实施转义流程

使用 LIKE 谓词的字符串 (% 、 _) 作为占位符的输入值时,除非它得到正确处理,否则它将作为通配符工作,因此有必要根据需要提前实施转义过程。这是必须将通配符用作单个字符 ("%" 或 "_") 的转义过程。

实际的转义过程是使用 ESCAPE 子句执行的,如下面的示例代码所示。

使用 LIKE 时的 ESCAPE 流程示例

// 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
        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("_", "@_"); // Escape _ in info which was received as input
        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.在无法使用占位符的情况下对 SQL 命令使用外部输入

当执行 SQL 语句时,其中的进程目标是数据库对象,如表创建/删除等,占位符不能用于表名称的值。基本上,不应使用从外部输入的任意字符串来设计数据库,以防占位符无法用于该值。

当由于规格或功能的限制而无法使用占位符时,无论输入值是否危险,都应在执行前进行验证,并且需要实施必要的流程。

基本上,

  1. 当用作字符串参数时,应该对字符执行转义或引用过程。
  2. 作为数值参数使用时,请验证是否不包括除数值以外的字符。
  3. 作为标识符或命令使用时,请验证是否不包括不能使用的字符,包括 1

都要执行检查。

4.5.3.3.采用对策,防止数据库被意外覆盖

如果通过 SQLiteOpenHelper#getReadableDatabase 、 getWriteableDatabase 获取数据库实例,则数据库可使用任一方法 [18] 以可读/可写状态打开。此外,Context#openOrCreateDatabase, SQLiteDatabase#openOrCreateDatabase 等也是如此。

[18]getReableDatabase() 返回可由 getWritableDatabase 获取的同一对象。如果由于磁盘已满等原因而无法生成可写对象,则此规范将返回只读对象。(getWritableDatabase() 在磁盘已满等情况下,将是执行错误。)

这意味着,应用程序操作可能会意外覆盖数据库的内容,或者由于实施中的缺陷而导致数据库的内容被覆盖。基本上,它可以由应用程序的规范和实施范围支持,但当实施只需要读取功能的功能时,如应用程序的搜索功能、通过只读方式打开数据库,它可能会导致简化设计或检查,并进一步提高应用程序质量,因此建议视具体情况而定。

具体来说,通过指定 OPEN_READONLY 至 SQLiteDatabase#openDatabase 来打开数据库。

以只读方式打开数据库。

    ...Abbreviation ...

    // Open DB(DB should be created in advance)
    SQLiteDatabase db
        = SQLiteDatabase.openDatabase(SQLiteDatabase.getDatabasePath("Sample.db"), null, OPEN_READONLY);

4.5.3.4.根据应用程序要求验证数据库的输入/输出数据的有效性

SQLite 是容差类型的数据库,可将字符类型数据存储到数据库中声明为整数的列中。对于数据库中的数据,包括数值类型的所有数据作为纯文本的字符数据存储在数据库中。因此,可以对整数类型列执行字符串类型搜索。(如‘%123%’等)此外,SQLite 中的值限制(有效性验证)不可信,因为在某些情况下可以输入长于限制的数据,例如 VARCHAR(100)。

因此,使用 SQLite 的应用程序需要非常谨慎地处理数据库的此特性,并且需要根据应用程序要求采取操作,而不是将意外数据存储到数据库或不获取意外数据。对策如以下两个要点所示。

  1. 在数据库中存储数据时,请验证类型和长度是否匹配。
  2. 从数据库获取值时,请验证数据是否超出了预期的类型和长度。

下面是验证输入值是否大于 1 的代码示例。

验证输入值是否大于 1(从 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.注意事项 - 存储到数据库中的数据

在 SQLite 实施中,将数据存储到文件时的操作如下所示。

  • 包括数值类型的所有数据作为纯文本的字符数据存储在数据库文件中。
  • 在对数据库执行数据删除时,数据本身不会从数据库文件中删除。(仅添加删除标记。)
  • 更新数据时,不会删除更新前的数据,而是仍保留在数据库文件中。

因此,“必须删除”的信息可能仍保留在数据库文件中。即使在这种情况下,也应根据本指南采取应对措施,当启用 Android 安全功能时,包括其他应用程序的第三方可能不会直接访问数据/文件。但是,考虑到这样的情况:如果存储了对业务产生巨大影响的数据,则应考虑通过像根权限这样的 Android 保护系统来提取文件;应考虑不依赖 Android 保护系统的数据保护。

如上所述,即使获取设备的根权限,需要保护的重要数据也不应存储在 SQLite 的数据库中。如果需要存储重要数据,则必须实施应对措施或加密整个数据库。

当需要加密时,存在许多超出本指南范围的问题,例如处理用于加密或代码模糊的密钥,因此,目前建议在开发处理具有巨大业务影响的数据的应用程序时咨询专家。

请参阅“4.5.3.6.此处介绍了加密数据库[参考] 加密 SQLite 数据库 (SQLCipher for Android)”。

4.5.3.6.[参考] 加密 SQLite 数据库 (SQLCipher for Android)

SQLCipher 由 Zetetic LLC 开发,提供 SQLite 数据库的透明 256 位 AES 加密。它是以 C 语言实施的 SQLite 扩展库,并使用 OpenSSL 进行加密。它还为 Obj-C、Java、Python 和其他语言提供 API。除了商业版本外,还提供开源版本(称为“社区版本”),并可用于具有 BSD 许可证的商业用途。它支持多种平台,包括 Windows、Linux、macOS 等,在移动领域,除 Android 外,它还广泛用于 Nokia/QT 和 Apple 的 iOS。

在这些版本中,专门为 Android 使用打包了 SQLCipher for Android [19]。虽然可以通过从可用源代码编译来创建内容,但库也以 AAR 格式 (android-database-sqlcipher-xxxx.AAR) 分发,这对于简单使用 [20] 可能更加方便。某些标准 SQLite API 可更改为与 SQLCipher 匹配,以使开发人员能够使用通过与平常相同的编码进行加密的数据库。本节简要介绍如何使用 AAR 格式的库。

参考:https://www.zetetic.net/sqlcipher/

[19]https://github.com/sqlcipher/android-database-sqlcipher
[20]在这些说明中,xxxx 是库的版本号,而本指南撰写时的最新版本是 3.5.9。以下说明假定使用此版本。
如何使用

Android Studio 中使用以下过程启用 SQLCipher。

  1. 将 android-database-sqlcipher-3.5.9.aar 放在应用程序的库目录中。(https://www.zetetic.net/sqlcipher/open-source/)

  2. 在应用程序/Gradle 中指定依赖性。

    dependencies {
        : 
        implementation 'net.zetetic:android-database-sqlcipher:3.5.9@aar'
        : 
    }
    
  3. 导入 net.sqlcipher.database *,而不是一般的 android.database.sqlite.*。(android.database.Cursor 可以在不做任何更改的情况下使用。)

  4. 在使用数据库之前,请加载并初始化库,并在打开数据库时指定密码。

下面显示的代码用于执行使用数据库的初始化过程。在活动使用数据库之前,假定调用 SQLCipherInitializer.Initialize()。首先,调用 SQLiteDatabase.loadLibs(this), 然后加载并初始化所需的库。此外,当使用 SQLiteDatabase.openOrCreateDatabase() 打开数据库时,将传递密码。数据库使用根据此处提供的密码生成的加密密钥进行加密。此处的要点是,以纯文本格式创建的数据库以后不能转换为加密数据库,并且必须在创建数据库时指定密码。

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);
    }
}

上面显示了使用 SQLiteDatabase.openOrCreateDatabase() 的示例,而不是 SQLiteOpenHelper#getWritableDatabase() 和 SQLiteOpenHelper#getReadableDatabase(),并且已修改 API,以便可以作为参数传递密码。在任一情况下,如果密码指定为 null,则会创建正常 SQLite 数据库而不加密数据库。

另一个关键要点是 Context#openOrCreateDatabase() 无法使用。因此,无法强制为数据库文件设置保护模式,也无法强制在软件包的本地目录中创建数据库。因此,当使用如上例所示的 SQLiteDatabase.openOrCreateDatabase() 创建数据库时,建议使用 getDatabasePath(),在程序包本身的数据库目录中创建数据库。另一方面,不会对 SQLiteOpenHelper 的构造函数 API 进行任何修改,如果使用此方法,将在数据包的本地目录中创建数据库,方法与 android.database.sqlite.SQLiteOpenHelper 相同。

4.6.处理文件

根据 Android 安全设计理念,文件仅用于实现信息持久性和临时保存(缓存),并且原则上应是私有的。在应用程序之间交换信息不应直接访问文件,而应通过应用程序间的链接系统(如内容提供器或服务)进行交换。通过使用此功能,可以实现应用程序间的访问控制。

由于无法在 SD 卡等外部存储设备上执行足够的访问控制,因此应仅限于在功能方面必须这样做时使用,例如处理大文件或将信息传输到其他位置(个人电脑等)。基本上,包含敏感信息的文件不应保存在外部存储设备中。如果需要以任何速率将敏感信息保存在外部设备的文件中,则必须采取加密等应对措施,但此处未提及。

4.6.1.示例代码

如上所述,文件在原则上应该是保密的。但是,有时其他应用程序应直接读/写文件,原因有多种。表 4.6.1 中显示了从安全角度进行分类和比较的文件类型。根据文件存储位置或其他应用程序的访问权限,这些文件分为 4 种类型。每个文件类别的示例代码如下所示,其中还添加了每个示例代码的说明。

表 4.6.1 从安全角度进行的文件类别划分和比较
文件类别 对其他应用程序的访问权限 存储位置 概述
私有文件 不适用 在应用程序目录中
  • 只能在一个应用程序中读取和写入。
  • 可以处理敏感信息。
  • 文件在原则上应为这种类型。
读出公共文件 读出 在应用程序目录中
  • 其他应用程序和用户可以读取。
  • 可在应用程序外部披露的信息将被处理。
读写公共文件 读出/写入 在应用程序目录中
  • 其他应用程序和用户可以读取和写入。
  • 从安全和应用程序设计的角度来说,不应使用它。
外部存储设备(读写公用) 读出/写入 外部存储设备,如 SD 卡
  • 无访问控制。
  • 其他应用程序和用户始终可以读取/写入/删除文件。
  • 使用量应是最低要求。
  • 可以处理相对大型的文件。

4.6.1.1.使用私有文件

在这种情况下,使用的是只能在同一应用程序中读取/写入的文件,是一种非常安全的文件使用方法。原则上,无论文件中存储的信息是否是公开的,请尽可能保持文件的私密性,并且在与其他应用程序交换必要信息时,都应使用其他 Android 系统(内容提供器、服务)完成。

要点:

  1. 文件必须在应用程序目录中创建。
  2. 文件的访问权限必须设置为私有模式,才能不被其他应用程序使用。
  3. 可以存储敏感信息。
  4. 有关要存储在文件中的信息,请小心、安全地处理文件数据。
PrivateFileActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.使用公用只读文件

在这种情况下,会使用文件将内容披露给大量未指定的应用程序。如果您遵循以下要点来实施,那么这也是相对安全的文件使用方法。请注意,使用 MODE_WORLD_READABLE 变量创建公共文件在 API 等级 17 和更高版本中已弃用,并将触发 API 等级 24 和更高版本中的安全异常;因此,最好使用内容提供器的文件共享方法。

要点:

  1. 文件必须在应用程序目录中创建。
  2. 文件的访问权限必须设置为对其他应用程序只读。
  3. 不得存储敏感信息。
  4. 有关要存储在文件中的信息,请小心、安全地处理文件数据。
PublicFileActivity.java
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.使用公共读/写文件

这种文件允许大量未指定的应用程序进行读写访问。

大量未指定的应用程序可以读取和写入,这带来的风险不言自明。恶意软件也可以读写,因此永远不能保证数据的可信度和安全性。此外,即使没有恶意操作,也无法控制文件中的数据格式或写入时间。因此,这种类型的文件在功能方面几乎不实用。

如上所述,从安全性和应用程序设计的角度来说,无法安全地使用读写文件,因此应避免使用读写文件。

要点:

  1. 不得创建允许从其他应用程序进行读/写访问的文件。

4.6.1.4.使用外部存储(读写公共)文件

在 SD 卡等外部存储器中存储文件时会出现这种情况。在存储相对巨大的信息(放置从 Web 下载的文件)或将信息导出到外部(备份等)时,应该使用此功能。

“外部存储文件(读写公用)”具有与大量未指定应用程序可访问的“读写公用文件”相同的特征。此外,它与声明使用 android.permission.WRITE_EXTERNAL_STORAGE 权限的应用程序“读写公用文件”具有相同的特性。因此,应尽量减少“外部存储文件(读写公用)”的使用。

作为 Android 应用程序的惯例,备份文件最可能是在外部存储设备中创建的。但是,如上所述,外部存储器中的文件存在被其他应用程序(包括恶意软件)篡改/删除的风险。因此,在输出备份的应用程序中,必须在应用程序规格或设计方面采取一些措施,以最大程度地降低风险,显示“尽快将备份文件复制到个人电脑等安全位置”之类的提醒是有必要的。

要点:

  1. 不得存储敏感信息。
  2. 文件必须存储在每个应用程序的唯一目录中。
  3. 有关要存储在文件中的信息,请小心、安全地处理文件数据。
  4. 作为规范,应禁止请求应用程序写入文件。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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.请参阅“3.2 谨慎且安全地处理输入数据”。
            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);
    }
}

可使用的示例代码

ExternalUserActivity.java
package org.jssec.android.file.externaluser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

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.NameNotFoundException;
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";

    @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 3 *** Regarding the information to be stored in files, handle file data carefully and securely.
            // Omitted, since this is a sample.请参阅“3.2 谨慎且安全地处理输入数据”。
            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;
    }
}
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" >
    
    <!-- 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>

4.6.2.规则手册

处理文件应遵循以下规则。

  1. 原则上,文件必须创建为私有文件(必需)
  2. 不能创建允许从其他应用程序进行读/写访问的文件(必需)
  3. 应尽量减少使用存储在外部设备(例如 SD 卡)中的文件(必需)
  4. 设计应用程序时应考虑文件范围(必需)

4.6.2.1.原则上,文件必须创建为私有文件(必需)

如“4.6.处理文件”和“4.6.1.1.使用私有文件”中提到的,无论要存储的信息内容如何,原则上都应将文件设置为私有。从 Android 安全设计的角度来看,交换信息及其访问控制应在 Android 系统中完成,如内容提供器和服务等。如果存在不可能的原因,则应考虑由文件访问权限替代。

请参阅每个文件类型的示例代码和以下规则项目。

4.6.2.2.不得创建允许从其他应用程序进行读/写访问的文件(必需)

如“4.6.1.3.使用公用读/写文件”中所述,如果允许其他应用程序读/写文件,则无法控制文件中存储的信息。因此,从安全性和功能/设计角度考虑,不应使用读/写公共文件共享信息。

4.6.2.3.应尽量减少使用存储在外部设备(例如 SD 卡)中的文件(必需)

如“4.6.1.4.使用外部存储(读写公共文件)文件”将文件存储在 SD 卡等外部存储设备中,从安全和功能的角度来说,会导致潜在问题。另一方面,与应用程序目录相比,SD 卡可以处理范围较长的文件,这是唯一一个始终可用于将数据导出到应用程序外部的存储。因此,可能有许多情况无法使用它,具体取决于应用程序的规范。

在外部存储设备中存储文件时,如果考虑到未指定的大量应用程序,并且用户可以读/写/删除文件,则设计应用程序时有必要考虑下面的要点以及示例代码中提到的要点。

  • 原则上,不应将敏感信息保存在外部存储设备的文件中。
  • 如果敏感信息保存在外部存储设备的文件中,则应对其进行加密。
  • 如果保存在外部存储设备文件中的信息被其他应用程序或用户篡改,会出现问题,则应使用电子签名进行保存。
  • 在外部存储设备中读取文件时,请在验证数据的安全性后使用数据进行读取。
  • 应用程序应设计为始终可以删除外部存储设备中的文件。

请参阅“4.6.2.4.设计应用程序时应考虑文件范围(必需)。”

4.6.2.4.设计应用程序时应考虑文件范围(必需)

以下用户操作将删除应用程序目录中保存的数据。它与应用程序的范围一致,并且其独特之处在于它比应用程序的范围更短。

  • 正在卸载应用程序。
  • 删除每个应用程序的数据和缓存(设置 > 应用程序 > 选择目标应用程序。)

保存在 SD 卡等外部存储设备中的文件,其范围比应用程序的范围要长,这一点很明显。此外,还需要考虑以下情况。

  • 用户删除文件
  • 拔掉/替换/卸载 SD 卡
  • 恶意软件删除文件

如上所述,由于文件根据其保存位置,范围会有所不同,这不仅关系到保护敏感信息,而且还关系到帮助应用程序实现正确的行为,因此有必要选择文件保存位置。

4.6.3.高级主题

4.6.3.1.通过文件描述符共享文件

有一种方法可以通过文件描述符共享文件,而不允许其他应用程序访问公共文件。此方法可用于内容提供器和服务。对手应用程序可以通过文件描述符读取/写入文件,这些描述符可通过在内容提供器或服务中打开私有文件来获取。

其他应用程序直接访问的文件共享方法与文件描述符的文件共享方法之间的比较,如下表 4.6.2 所示。访问权限的变化和允许访问的应用程序的范围可视为优点。尤其是从安全角度来看,这是一个很好的优点,允许访问的应用程序可以详细控制。

表 4.6.2 应用程序间文件共享方法的比较
文件共享方法 变体或访问权限设置 允许访问的应用程序范围
允许其他应用程序直接访问文件的文件共享
  • 读入
  • 写入
  • 读入 + 写入
平等授予所有应用程序访问权限
通过文件描述符共享文件
  • 读入
  • 写入
  • 仅添加
  • 读入 + 写入
  • 读入 + 仅添加
可以控制是否向尝试单独和临时访问内容提供器或服务的应用程序授予访问权限

这在上述两种文件共享方法中都很常见,向其他应用程序授予文件的写入权限时,很难保证文件内容的完整性。当多个应用程序并行写入时,文件内容的数据结构可能会被破坏,并且应用程序无法正常工作。因此,在与其他应用程序共享文件时,最好只授予只读权限。

下面将发布通过内容提供器共享文件的实施示例及其示例代码。

要点

  1. 源应用程序是内部应用程序,因此可以保存敏感信息。
  2. 即使结果来自仅供内部使用的内容提供器应用程序,也要验证结果数据的安全性。
InhouseProvider.java
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
                    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.目录的访问权限设置

以上内容重点介绍了文件的安全注意事项。此外,还必须考虑作为文件容器的目录的安全性。下面说明了目录访问权限设置的安全注意事项。

在 Android 中,有一些方法可用于获取/创建应用程序目录中的子目录。主要内容如下表 4.6.3 所示。

表 4.6.3应用程序目录中获取/创建子目录的方法
  指定对其他应用程序的访问权限 用户删除
Context#getFilesDir() 不可能(仅执行权限) “设置” > “应用程序” > 选择目标应用程序 > “清除数据”
Context#”getCacheDir() 不可能(仅执行权限) “设置” > “应用程序” > 选择目标应用程序 > “清除缓存”(也可通过“清除数据”删除它。)
Context#getDir(String name, int mode) 模式 MODE_PRIVATE、MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 可以指定为 MODE “设置” > “应用程序” > 选择目标应用程序 > “清除数据”

此处特别需要注意的是 Context#getDir() 设置的访问权限。如文件创建中所述,基本上还应从安全设计的角度将目录设置为私有。共享信息取决于访问权限设置时,可能会产生意外的副作用,因此应采用其他方法作为信息共享。

MODE_WORLD_READABLE

这是一个标志,用于向所有应用程序授予对目录的只读权限。因此,所有应用程序都可以在目录中获取文件列表和单个文件属性信息。由于机密文件不能放在这些目录中,通常不能使用此标记。[21]

[21](12) MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 在 API 等级 17 和更高版本中已弃用,而在 API 等级 24 和更高版本中,其使用将触发安全异常。
MODE_WORLD_WRITEABLE

此标志授予其他应用程序对目录的写入权限。所有应用程序都可以在目录中创建/移动/[22]重命名/删除文件。这些操作与文件本身的访问权限设置(读/写/执行)没有关系,因此有必要注意,操作只能在具有目录写入权限的情况下完成。此标志允许其他应用程序任意删除或替换文件,因此通常不能使用。[21]

[22]文件不能在安装点上移动(例如,从内部存储移动到外部存储)。因此,不能将受保护的文件从内部存储移到外部存储。

有关表 4.6.3“用户删除”的信息,请参阅“4.6.2.4.设计应用程序时应考虑文件范围(必需)。”

4.6.3.3.共享首选项和数据库文件的访问权限设置

共享首选项和数据库也包含文件。有关对文件的访问权限设置,此处也进行了说明。因此,共享首选项和数据库应创建为与文件相同的私有文件,共享内容应通过 Android 的应用程序间的链接系统实现。

下面显示了共享首选项的使用示例。共享首选项由 MODE_PRIVATE 创建为私有文件。

设置对共享首选项文件的访问限制的示例。

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: 基本上,要指定 MODE_PRIVATE 模式。
        SharedPreferences preference = getSharedPreferences(
            PREFERENCE_FILE_NAME, MODE_PRIVATE);

        // Example of writing preference which value is charcter string.
        Editor editor = preference.edit();
        editor.putString("prep_key", "prep_value"); // key:"prep_key", value:"prep_value"
        editor.commit();

请参阅“4.5.使用 SQLite”用于数据库。

4.6.3.4.有关 Android 4.4(API 等级 19)和更高版本中的外部存储访问的规范更改

自 Android 4.4(API 等级 19)起,有关外部存储访问的规范已更改为以下内容。

(1) 如果应用程序需要对外部存储介质上的特定目录进行读/写,则不需要使用 <uses-permission> 来声明 WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE 权限。(已更改)

(2) 如果应用程序需要读取外部存储介质上其他目录(而不是其特定目录)上的文件,则需要使用 <uses-permission> 声明 READ_EXTERNAL_STORAGE 权限。(已更改)

(3) 如果应用程序需要将文件写入主外部存储介质上的特定目录以外的其他目录,则需要使用 <uses-permission> 来声明 WRITE_EXTERNAL_STORAGE 权限。

(4) 在辅助外部存储介质上,应用程序不能将文件写入除自身特定目录之外的其他目录。

在该规范中,是否需要权限许可取决于 Android 操作系统的版本。因此,如果应用程序支持包括 Android 4.3 和 4.4 在内的版本,可能会带来令人愉快的情况,即应用程序不需要用户的权限。因此,建议使用与段落 (1) 相对应的应用程序,如下面所示,使用 <uses-permission> 的 maxSdkVersion 属性。

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.Android 7.0(API 等级 24)中修订的规范,用于访问外部存储介质上的特定目录

在运行 Android 7.0(API 等级 24)或更高版本的设备上,引入了一个称为“范围目录访问 API”的新 API。作用域目录访问允许应用程序未经许可访问外部存储媒体上的特定目录。

在作用域目录访问中,环境类中定义的目录将作为参数传递给 StorageVolume#createAccessIntent 方法以创建意图。通过 startActivityForResult 发送此意图,您可以启用以下情况:终端屏幕上出现请求访问权限的对话框,并且如果用户授予权限,则每个存储卷上的指定目录将变得可访问。

表 4.6.4 可通过作用域目录访问进行访问的目录
DIRECTORY_MUSIC 常规音乐文件的标准位置
DIRECTORY_PODCASTS 播客标准目录
DIRECTORY_RINGTONES 铃声标准目录
DIRECTORY_ALARMS 闹钟标准目录
DIRECTORY_NOTIFICATIONS 通知的标准目录
DIRECTORY_PICTURES 图片的标准目录
DIRECTORY_MOVIES 电影标准目录
DIRECTORY_DOWNLOADS 用户下载文件的标准目录
DIRECTORY_DCIM 摄像机拍摄的图像/视频文件的标准目录
DIRECTORY_DOCUMENTS 用户创建文档的标准目录

如果应用程序要访问的位置位于上述目录之一,并且应用程序在 Android 7.0 或更高版本的设备上运行,则建议使用作用域目录访问,原因如下。对于必须继续支持 Android 7.0 之前的设备的应用程序,请参阅“4.6.3.4.有关 Android 4.4(API 等级 19)及更高版本中的外部存储访问的规范更改”部分中列出的在 AndroidManifest 中的示例代码。

  • 当授予访问外部存储器的权限时,应用程序能够访问预期目标以外的目录。
  • 使用存储访问框架要求用户选择可访问的目录会导致一个繁琐的过程,用户必须在每次访问时配置一个选择器。此外,当授予对外部存储器的根目录的访问权限时,整个存储器将变得可访问。

4.7.使用可浏览意图

Android 应用程序可设计为从与网页链接对应的浏览器启动。此功能称为“可浏览意图”。通过在清单文件中指定 URI 方案,应用程序将响应对具有其 URI 方案的链接(用户点击等)的转换,并以链接作为参数启动应用程序。

此外,使用 URI 方案从浏览器中启动相应应用程序的方法不仅在 Android 中支持,在 iOS 和其他平台中也支持,而且通常用于 Web 应用程序和外部应用程序之间的链接等。例如,以下 URI 方案在 Twitter 应用程序或 Facebook 应用程序中定义,相应的应用程序在 Android 和 iOS 中从浏览器启动。

表 4.7.1 URI 方案和对应的应用程序
URI 方案 相应的应用程序
fb:// Facebook
twitter:// Twitter

考虑到链接和便利性,此功能似乎非常方便,但也有一些风险,因为恶意第三方可能滥用此功能。假设如下:它们通过准备带有链接的恶意网站(此链接的 URL 具有不正确参数)来滥用应用程序功能,或者通过欺骗智能手机所有者安装响应相同 URI 方案的恶意软件来获取 URL 中包含的信息。

针对这些风险,在使用“可浏览意图”时,需要注意一些要点。

4.7.1.示例代码

下面显示了使用“可浏览意图”的应用程序的示例代码。

要点:

  1. (网页)不得包含敏感信息。
  2. 小心、安全地处理 URL 参数。
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
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.请参阅“3.2 谨慎且安全地处理输入数据”。
            String userID = "User ID = " +  uri.getQueryParameter("user");
            TextView tv = (TextView)findViewById(R.id.text_userid);
            tv.setText(userID);
        }
    }
}

4.7.2.规则手册

使用“可浏览意图”时,请遵循下面列出的规则。

  1. (网页)敏感信息不得包含在相应链接的参数中(必需)
  2. 小心、安全地处理网址参数(必需)

4.7.2.2.小心、安全地处理网址参数(必需)

发送到应用程序的 URL 参数并非始终来自合法的网页,因为与 URI 方案匹配的链接不仅可以由开发人员创建,而且可以由任何人创建。此外,没有方法可以验证 URL 参数是否是从有效的网页发送的。

因此,在使用 URL 参数之前必须验证其安全性,例如检查是否包括意外值。

4.8.输出日志到 LogCat

Android 中有一种称为 LogCat 的日志记录机制,系统日志信息和应用程序日志信息都会输出到 LogCat。可以从同一设备中的其他应用程序中读取 LogCat 中的日志信息 [23],因此将敏感信息输出到 Logcat 的应用程序被视为存在信息泄露漏洞。敏感信息不应输出到 LogCat。

[23]使用 READ_LOGS 权限声明的应用程序可以读取输出到 LogCat 的日志信息。但是,在 Android 4.1 和更高版本中,无法读取由其他应用程序输出的日志信息。但智能手机用户可以通过 ADB 读取到 logcat 的每个日志信息输出。

从安全角度来看,在发行版应用程序中,最好不要输出任何日志。但是,即使是在发行版应用程序中,在某些情况下也会由于某些原因而输出日志。在本章中,我们介绍了一些以安全方式将消息输出到 LogCat 的方法,即使在发行版应用程序中也是如此。除此说明外,请参阅 “4.8.3.1.发布版本应用程序中日志输出的两种考虑方法”。

4.8.1.示例代码

之后,我们将介绍在发行版本应用程序中由 ProGuard 控制 LogCat 的日志输出的方法。ProGuard 是一种优化工具,可自动删除不必要的代码,如未使用的方法等。

在 android.util.Log 类中,日志输出方法有五种类型,即 Log.e()、Log.w()、Log.i()、Log.d()、Log.v()。对于日志信息,应将故意输出日志信息(以下简称为操作日志信息)与日志记录区分开,后者不适用于发行版本应用程序,如调试日志(以下简称为开发日志信息)。建议使用 Log.e()/w()/i() 输出操作日志信息,并使用 Log.d()/v() 输出开发日志。请参阅“4.8.3.2.日志级别和日志输出方法的选择标准”,以了解正确使用五种类型的日志输出方法的详细信息,此外,还请参阅“4.8.3.3.调试日志和详细日志并非总是自动删除”。

下面是如何以安全方式使用 LogCat 的示例。此示例包括用于输出调试日志的 Log.d() 和 Log.v()。如果应用程序用于发行,这两种方法将自动删除。在此示例代码中,ProGuard 用于自动删除调用 Log.d()/v() 的代码块。

要点:

  1. Log.e()/w()/i()、System.out/err 不得输出敏感信息。
  2. 如果需要,敏感信息应由 Log.d()/v() 输出。
  3. 不应使用 Log.d()/v()(用于替换或比较)的返回值。
  4. 在为发行构建应用程序时,您应引入自动删除代码中的不适当日志记录方法(如 Log.d() 或 Log.v() 的机制)。
  5. 必须在版本构建配置中创建(公共)版本的 APK 文件。
ProGuardActivity.java
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(...);
}

*** 要点 5 *** 必须在版本构建配置中创建(公共)版本的 APK 文件。

_images/image49.png

图 4.8.1 如何创建发行版本应用程序

开发版本应用程序(调试版本)和发行版本应用程序(发行版本)之间的 LogCat 输出差异如 图 4.8.2 所示。

_images/image50.png

图 4.8.2 开发版本应用程序和发行版本应用程序之间的 LogCat 输出差异

4.8.2.规则手册

输出日志消息时,请遵循以下规则。

  1. 操作日志信息中不得包含敏感信息(必需)
  2. 在构建发行版本时,创造构建系统以自动删除输出开发日志信息的代码(推荐)
  3. 输出可引发对象时使用 Log.d()/v() 方法(推荐)
  4. 仅对日志输出使用 android.util.Log 类的方法(推荐)

4.8.2.1.操作日志信息中不得包含敏感信息(必需)

输出到 LogCat 的日志可从其他应用程序读取,因此发行版应用程序不应输出用户登录信息等敏感信息。在开发过程中,不需要编写将敏感信息输出到日志的代码,也不必在发布前删除所有此类代码。

要遵循此规则,首先不要在操作日志信息中包括敏感信息。此外,在构建发行版时,建议构建系统以删除输出敏感信息的代码。请参阅“4.8.2.2.在构建发行版时,创造构建系统以自动删除输出开发日志信息的代码(推荐)”。

4.8.2.2.在构建发行版时,创造构建系统以自动删除输出开发日志信息的代码(推荐)

在开发应用程序时,如果敏感信息输出到日志以检查流程内容和进行调试,例如,临时操作导致复杂逻辑的过程、程序内部状态的信息、通信协议的通信数据结构,则有时这是更好的选择。在开发过程中将敏感信息输出为调试日志无关紧要,在这种情况下,应在发布前删除相应的日志输出代码,如“4.8.2.1.中所述。操作日志信息中不得包含敏感信息(必需)”。

要在发行版本时删除必然输出开发日志信息的代码,应构建使用某些工具自动执行代码删除的系统。“4.8.1.示例代码”中描述的 Proguard,可用于此方法。如下所述,ProGuard 删除代码时有一些值得注意的要点。在这里,它应该将系统应用于根据“4.8.3.2.日志级别和日志输出方法的选择标准”由 Log.d()/v() 输出开发日志信息的应用程序。

ProGuard 自动删除不必要的代码,如未使用的方法。通过将 Log.d()/v() 指定为 -assumenosendeffects 选项的参数,对 Log.d()、Log.v() 的调用将被视为不必要的代码,并且将删除这些代码。

通过将 -assumenosendeffects 指定为 Log.d()/v(),使其成为自动删除目标。

-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
}

如果使用此自动删除系统,请注意,再使用 Log.v(), Log.d() 的返回值时,Log.v()/d() 代码不会被删除,因此不应使用。例如,在下一个检查代码中不会删除 Log.v()。

指定要删除的 Log.v() 检查代码未删除。

int i = android.util.Log.v("tag", "message");
System.out.println(String.format("Log.v() returned %d.", i)); //Use the returned value of Log.v() for examination.

如果要重复使用源代码,应保持包括 ProGuard 设置在内的项目环境的一致性。例如,高于 ProGuard 设置时,预设 Log.d() 和 Log.v() 的源代码将自动删除。如果在未设置 ProGuard 的其他项目中使用此源代码,则不会删除 Log.d() 和 Log.v(),因此可能会泄露敏感信息。重新使用源代码时,应确保包括 ProGuard 设置在内的项目环境的一致性。

4.8.2.3.输出可引发对象时使用 Log.d()/v() 方法(推荐)

如“4.8.1.示例代码”和“4.8.3.2.日志级别和日志输出方法的选择标准”中所述,敏感信息不应通过 Log.e()/w()/i() 输出到日志。另一方面,在某些情况下,为了让开发人员输出程序异常的详细信息以进行记录,发生异常时,堆栈跟踪将通过 Log.e(…, Throwable tr)/w(…, Throwable tr)/i(…, Throwable tr) 输出到 LogCat。但是,敏感信息有时可能包括在堆栈跟踪中,因为它显示了程序的详细内部结构。例如,当 SQLiteException 按原样输出时,将阐明发出的 SQL 语句的类型,因此它可能会提供 SQL 注入攻击的线索。因此,建议在输出可引发对象时仅使用 Log.d()/Log.v() 方法。

4.8.2.4.仅对日志输出使用 android.util.Log 类的方法(推荐)

在开发期间您可以通过 System.out/err 输出日志,以验证应用程序是否按预期工作。当然,您可以通过 System.out/err 的 print()/println() 方法将日志输出到 LogCat,但强烈建议仅使用 android.util.Log 类的方法,原因如下。

在输出日志时,通常根据信息的紧急程度正确使用最合适的输出方法,并控制输出。例如,应使用严重错误、小心、简单应用程序的信息通知等类别。但是,在这种情况下,需要在发行时输出的信息(操作日志信息)和可能包括敏感信息(开发日志信息)的信息将通过相同的方法输出。因此,当删除输出敏感信息的代码时,可能会发生这样的情况:由于疏忽而导致某些删除操作未执行的危险。

此外,在使用 android.util.Log 和 System.out/err 进行日志输出时,与仅使用 android.util.Log 相比,需要考虑的内容将会增加,因此可能会出现一些错误,例如由于疏忽导致删除操作未执行。

要降低上述错误发生的风险,建议仅使用 android.util.Log 类方法。

4.8.3.高级主题

4.8.3.1.发布版本应用程序中日志输出的两种考虑方法

在发行版本应用程序中,有两种考虑日志输出的方法。其中一个日志永远不应输出,另一个日志是以后分析所必需的信息,应作为日志输出。从安全角度来看,任何日志都不应在发行版本应用程序中输出,但有时,即使出于各种原因,也会在发行版本应用程序中输出日志。每种考虑方式如下所述。

前者是“任何日志都不应输出”,这是因为在发行版本应用程序中输出日志没有太大价值,并且存在泄露敏感信息的风险。这是因为开发人员没有方法在 Android 应用程序操作环境中收集发行版本应用程序的日志信息,这与许多 Web 应用程序操作环境不同。根据这种想法,日志代码仅在开发阶段使用,并且所有日志代码在构建版本应用程序上都被删除。

后者是“必要信息应作为日志输出以供以后分析”。如果您的客户支持有任何问题或疑问,这是分析客户支持中的应用程序错误的最终选项。根据上述想法,有必要准备系统以防止人为错误并将其引入项目中,因为如果您没有系统,则必须记住要避免在发行版本应用程序中记录敏感信息。

有关日志记录方法的详细信息,请参阅以下文档。

贡献者/日志的代码样式指南

4.8.3.2.日志级别和日志输出方法的选择标准

Android 中的 android.util.Log 类中定义了五个级别的日志级别(错误、警告、信息、调试、详细)。在根据表 4.8.1(该表显示了日志级别和方法的选择标准)使用 android.util.Log 类输出日志消息时,应选择最合适的方法。

表 4.8.1 日志级别和日志输出方法的选择标准
日志级别 方法 要输出的日志信息 应用程序发行注意事项
错误 Log.e() 当应用程序处于危险状态时输出的日志信息。 用户可以参考左侧的日志信息,因此它们可以在开发版本应用程序和发行版本应用程序中输出。因此,不应在这些级别输出敏感信息。
警告 Log.w() 当应用程序面临意外严重情况时输出的日志信息。
信息 Log.i() 除上述信息外,还会输出日志信息,以通知应用程序状态的任何显著的更改或结果。
调试 Log.d() 程序的内部状态信息,需要临时输出,以便在开发应用程序时分析特定错误的原因。 左侧的日志信息仅供应用程序开发人员使用。因此,如果是发行版本应用程序,则不应输出此类信息。
详细 Log.v() 不适用于以上任何一项的日志信息。此处是指应用程序开发人员为多种用途输出的日志信息。例如,在输出要转储的服务器通信数据时。

有关日志记录方法的详细信息,请参阅以下文档。

贡献者/日志的代码样式指南

4.8.3.3.调试日志和详细日志并不总是自动删除

下面是 android.util.Log 类的开发人员参考中引用的内容 [24]

从最低到最高的详细程度顺序为 ERROR、WARN、INFO、DEBUG、VERBOSE。Verbose 不能编译到应用程序中,除非是在开发过程中。调试日志编译到其中,但在运行时剥离。始终保留错误、警告和信息日志。

[24]http://developer.android.com/reference/android/util/Log.html

在阅读上述文本后,一些开发人员可能会误解如下所示的日志类行为。

  • 当发行版本时,不编译 Log.v() 调用,详细日志永远不会输出。
  • Log.v() 调用已编译,但执行时不输出 DEBUG 日志。

但是,日志记录方法从不以以上方式运行,无论是否使用调试模式或发布模式编译,都将输出所有消息。如果仔细阅读文档,您将能够意识到文档的概要并非关于记录方法的行为,而是关于记录的基本策略。

在本章中,我们介绍了使用 ProGuard 以获得如上所述的预期结果的示例代码。

4.8.3.4.从集合中删除敏感信息

如果使用 ProGuard 构建以下代码以删除 Log.d() 方法,则需要记住,ProGuard 会保留构造用于记录消息的字符串的语句(代码的第一行),即使它删除了调用 Log.d() 方法的语句(代码的第二行)也是如此。

    String debug_info = String.format("%s:%s", "Sensitive information 1", "Sensitive information 2");
    if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);

以下内容详细显示了使用 ProGuard 构建上述代码的结果。实际上,没有 Log.d() 调用过程,但是您可以看到诸如“敏感信息 1”和 String#format() 方法的调用过程等字符串一致性定义未被删除,仍保留在该过程中。

    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

实际上,查找如上所述的分解 APK 文件和汇编日志输出信息的特定部件并不容易。但是,在某些处理非常机密信息的应用程序中,在某些情况下,此类进程不应保留在 APK 文件中。

您应如下所示实施您的应用程序,以避免在字节码中保留敏感信息。在发行版本中,编译器优化将完全删除以下代码。

    if (BuildConfig.DEBUG) {
        String debug_info = String.format("%s:%s", "Snsitive information 1", "Sensitive information 2");
        if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info);
    }

此外外,ProGuard 无法删除以下代码的日志消息(“结果:”+ 值)。

    Log.d(TAG, "result:"+ value);

在这种情况下,您可以按以下方式解决问题。

    if (BuildConfig.DEBUG) Log.d(TAG, "result:"+ value);

4.8.3.5.意图内容输出到 LogCat

使用活动时,必须注意,因为 ActivityManager 会将意图内容输出至 LogCat。请参阅“4.1.3.5.使用活动时的日志输出”。

4.8.3.6.输出到 System.out/err 的约束日志

System.out/err 方法将所有消息输出到 LogCat。Android 可以向 System.out/err 发送一些消息,即使开发人员未在其代码中使用这些方法,例如,在以下情况下,Android 会向 System.err 方法发送堆栈跟踪。

  • 使用 Exception#printStackTrace() 时
  • 当它隐式输出到 System.err
    时(当应用程序未捕获到异常时,系统会将其赋予 Exception#printStackTrace()。)

由于堆栈跟踪包括应用程序的唯一信息,因此您应适当地处理错误和异常。

我们介绍了一种更改 System.out/err 作为默认输出目标的方法。以下代码将 System.out/err 方法的输出重定向到构建发行版本应用程序时的位置。但是,您应该考虑此重定向是否会导致应用程序或系统出现故障,因为该代码会暂时覆盖 System.out/err 方法的默认行为。此外,此重定向仅对您的应用程序有效,对系统进程毫无作用。

OutputRedirectApplication.java
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.(在发行版本中,ProGuard 删除了下面的第 1 行。)
        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(...);
}

开发版本应用程序(调试版本)和发行版本应用程序(发行版本)之间的 LogCat 输出差异如 图 4.8.3 所示。

_images/image51.png

图 4.8.3 开发应用程序和发行应用程序之间 LogCat 输出中 System.out/err 的区别。

4.9.使用 WebView

WebView 使您的应用程序能够集成 HTML/JavaScript 内容。

4.9.1.示例代码

我们需要根据希望通过 WebView 显示的内容采取正确的操作,尽管我们可以轻松地显示网站和 html 文件。此外,我们还需要考虑 WebView 的显著功能带来的风险,例如 JavaScript - Java 对象绑定。

我们需要特别注意的是 JavaScript。(请注意,JavaScript 默认为禁用。我们可以通过 WebSettings#setJavaScriptEnabled() 启用它。)启用 JavaScript 后,可能存在恶意第三方获取设备信息和操作您的设备的风险。

以下是使用 WebView 应用程序的原则[25]

  1. 如果应用程序使用内部管理的内容,您可以启用 JavaScript。
  2. 上述情况以外,您不应启用 JavaScript。
[25]严格地说,如果可以保证内容是安全的,您就可以启用 JavaScript。如果内容是在内部管理的,则内容应保证安全性。而且公司可以保护它们的安全。换言之,我们需要做出业务代表的决定,以便为其他公司的内容启用 JavaScript。受信任合作伙伴开发的内容可能具有安全保证。但仍存在潜在风险。因此,负责人需要做出决定。

图 4.9.1 显示了根据内容特征选择示例代码的流程图。

_images/image52.png

图 4.9.1 选择 WebView 的示例代码的流程图

4.9.1.1.只显示 APK 中存储在 assets/res 目录下的内容

如果您的应用程序仅显示 apk 中存储在 assets/ 和 res/ 目录下的内容,您可以启用 JavaScript。

以下示例代码显示如何使用 WebView 显示存储在 assets/ 和 res/ 下的内容。

要点:

  1. 禁止访问文件(apk 中 assets/ 和 res/ 下的文件除外)。
  2. 您可以启用 JavaScript。
WebViewAssetsActivity.java
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.仅显示内部管理的内容

只有当您的 Web 服务和 Android 应用程序可以执行适当的操作来保护它们的安全时,您才可以启用 JavaScript 以仅显示内部管理的内容。

  • Web 服务端操作:

图 4.9.2 显示,您的 Web 服务只能引用内部管理的内容。此外,Web 服务还需要执行相应的安全操作。因为如果 Web 服务引用的内容存在风险,例如恶意攻击代码注入、数据操纵等,则存在潜在风险。

请参阅“4.9.2.1.仅在内部管理内容时启用 JavaScript(必需)”.

  • Android 应用程序端操作:

使用 HTTPS,只有在证书受信任的情况下,应用程序才应与托管 Web 服务建立网络连接。

以下示例代码是显示内部管理内容的活动。

_images/image53.png

图 4.9.2 可从应用程序访问的内容和不可访问的内容。

要点:

  1. 正确处理 WebView 中的 SSL 错误。
  2. (可选)启用 WebView 的 JavaScript。
  3. 将 URL 限制为仅 HTTPS 协议。
  4. 将 URL 限制为内部。
WebViewTrustedContentsActivity.java
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.显示非内部管理的内容

如果您的应用程序显示非内部管理的内容,请不要启用 JavaScript,因为可能存在访问恶意内容的风险。

以下示例代码是显示非内部管理内容的活动。

此示例代码显示用户通过地址栏输入的 URL 指定的内容。请注意,JavaScript 已禁用,发生 SSL 错误时连接会中止。错误处理与 “ 4.9.1.2.仅显示内部管理的内容”中相同,可了解 HTTPS 通信的详细信息。请参阅“5.4.通过 HTTPS 通信”,了解详细信息。

要点:

  1. 正确处理 WebView 中的 SSL 错误。
  2. 禁用 WebView 的 JavaScript。
WebViewUntrustActivity.java
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.规则手册

当您需要使用 WebView 时,请遵守以下规则。

  1. 仅在内部管理内容时启用 JavaScript(必需)
  2. 使用 HTTPS 与内部管理的服务器通信(必需)
  3. 禁用 JavaScript 以显示通过意图等接收的网址(必需)
  4. 正确处理 SSL 错误(必需)

4.9.2.1.仅在内部管理内容时启用 JavaScript(必需)

我们必须注意 WebView 是否启用 JavaScript。原则上,只有当应用程序将访问内部管理的服务时,我们才能启用 JavaScript。如果可以访问非内部管理的服务,则不能启用 JavaScript。

内部管理的服务

如果应用程序访问在内部开发的内容,并通过在内部管理的服务器进行分发,我们可以说内容只能由贵公司修改。此外,每个内容都需要是存储在具有适当安全性的服务器中的内容。

在这种情况中,我们可以在 WebView 上启用 JavaScript。请参阅“4.9.1.2.仅显示内部管理的内容”。

如果您的应用程序仅显示 apk 中存储在 assets/ 和 res/ 目录下的内容,您可以启用 JavaScript。请参阅“4.9.1.1.仅显示存储在 APK 中的资产/资源目录下的内容”。

非内部管理的服务

您不能认为您可以保护非内部管理的内容的安全。因此,您必须禁用 JavaScript。请参阅“4.9.1.3.显示非内部管理的内容”。

此外,如果内容存储在外部存储媒介(例如 microSD)中,则必须禁用 JavaScript,因为其他应用程序可以修改内容。

4.9.2.2.使用 HTTPS 与内部管理的服务器通信(必需)

您必须使用 HTTPS 与内部管理的服务器通信,因为恶意第三方可能会冒充服务。

请参阅“4.9.2.4.正确处理 SSL 错误(必需)”和“5.4.通过 HTTPS 通信”。

4.9.2.3.禁用 JavaScript 以显示通过意图等接收的网址(必需)

如果您的应用程序需要将从其他应用程序传递的 URL 显示为意图等,请不要启用 JavaScript。因为可能存在显示带有恶意 JavaScript 的恶意网页的风险。

4.9.1.2.一节中的示例代码。仅显示内部管理的内容”一节中的示例代码。使用固定值 URL 显示内部管理的内容,以确保安全。

如果需要显示从意图等收到的 URL,则必须确认 URL 属于内部管理的 URL。简而言之,应用程序必须检查具有正则表达式等白名单的 URL。此外,它还应该是 HTTPS。

4.9.2.4.正确处理 SSL 错误(必需)

当 HTTPS 通信发生 SSL 错误时,您必须终止网络通信并向用户发送错误通知。

SSL 错误显示无效的服务器认证风险或 MTIM(中间人攻击)风险。请注意,WebView 没有关于 SSL 错误的错误通知机制。因此,您的应用程序必须显示错误通知,以向用户通知风险。请参阅“4.9.1.2.仅显示内部管理的内容“和"4.9.1.3.显示非内部管理的内容”。

此外,您的应用程序必须终止与错误通知的通信。

换言之,您不得执行以下操作。

  • 忽略此错误以使事务与服务保持一致。
  • 重试 HTTP 通信而不是 HTTPS。

请参阅“ 5.4.通过 HTTPS 通信”中所述的详细信息。

WebView 的默认行为是在出现 SSL 错误时终止通信。因此,我们需要添加的内容是显示 SSL 错误通知。然后,我们可以正确处理 SSL 错误。

4.9.3.高级主题

4.9.3.1.Android 4.1 或更低版本中的 addJavascriptInterface() 导致的漏洞

4.2 以下 Android版本(API 等级 17)有一个 addJavascriptInterface() 引起的漏洞,这可能允许攻击者在 WebView 上通过 JavaScript 调用原生 Android 方法 (Java)。

如 “ 4.9.2.1.仅在内部管理内容时启用 JavaScript(必需)中所述,如果服务可以从内部控制之外访问服务,则不能启用 JavaScript。

在 Android 4.2(API 等级 17)或之后,脆弱性的措施已经被限制访问从 JavaScript 方法有 @JavascriptInterface 注释的 Java 源代码,而不是 Java 对象注入的所有方法。但是,如果服务可以从“4.9.2.1.”中提到的内部控制外访问服务,则必须禁用 JavaScript。

4.9.3.2.由文件方案导致的问题

如果使用带有默认设置的 WebView,则可以使用网页中的文件方案访问应用程序具有访问权限的所有文件,而不考虑页面来源。例如,恶意网页可以通过使用文件方案向应用程序的私有文件 uri 发送请求来访问存储在应用程序私有目录中的文件。

对策是按照“4.9.2.1.仅在内容由内部管理时启用 JavaScript(必需)”中的说明,如果服务可以从非内部控制的服务访问,则禁用 JavaScript。这样做是为了防止发送恶意文件方案请求。

对于 Android 4.1(API 级别 16) 或更高版本,可以使用 setAllowFileAccessFromFileURLs() 和 setAllowUniversalAccessFromFileURLs() 来限制通过文件方案的访问。

禁用文件方案

        webView = (WebView) findViewById(R.id.webview);
        webView.setWebViewClient(new WebViewUnlimitedClient());
        WebSettings settings = webView.getSettings();
        settings.setAllowUniversalAccessFromFileURLs(false);
        settings.setAllowFileAccessFromFileURLs(false);

4.9.3.3.使用 Web 消息时指定发件人来源

Android 6.0(API 等级 23)添加了一个 API 来实现 HTML5 Web 消息传递。Web 消息是 HTML5 中定义的框架,用于在不同浏览环境之间发送和接收数据。[26]

[26]http://www.w3.org/TR/webmessaging/

添加到 WebView 类的 postWebMessage() 方法是一种通过 Web 消息传递定义的跨域消息传递协议,处理数据传输的方法。

此方法从已读入 WebView 的浏览环境发送由其第一个参数指定的消息对象;但是,在这种情况下,必须将发送者的来源指定为第二个参数。如果指定的原始位置 [27] 与发送者环境中的原始位置不一致,则不会发送消息。通过以这种方式限制发送者来源,此机制旨在防止将邮件传递给意外发送者。

[27]“origin”是一个 URL 方案,其中包含主机名和端口号。有关详细定义,请参阅 http://tools.ietf.org/html/rfc6454

但是,请务必注意,通配符可以指定为 postWebMessage() 方法中的源。[28] 如果指定了通配符,则不会检查邮件的发件人来源,并且可以从任意来源发送邮件。在将恶意内容读入 WebView 的情况下,如果在发送重要邮件时没有原始限制,可能会导致各种类型的伤害或损害。因此,在使用 WebView 进行 Web 消息传递时,最好在 postWebMessage() 方法中明确指定特定来源。

[28]请注意,Uri.EMPTY 和 Uri.parse("") 可作为通配符使用(在编写 2016 年 9 月 1 日版本时)。

4.9.3.4.WebView 中的安全浏览

安全浏览是 Google 提供的一项服务,当用户尝试访问恶意软件页面、钓鱼网站或其他不安全网页时,该服务会显示警告页面。

_images/image94.png

图 4.9.3 尝试在 Android 版 Chrome 中访问不安全网页时会显示警告页面。

目前,安全浏览功能不仅可用于 Android 和其他浏览器应用程序的 Chrome,还可用于应用程序中使用的 WebView。但是,需要特别注意,因为可用于 WebView 的组件因系统的 Android 操作系统版本而异,因此,安全浏览的支持程度也会有所不同。下表显示了对 Android 操作系统标准 WebView 和安全浏览版本的支持信息。

表 4.9.1 Android 操作系统版本和标准 WebView 支持
Android 操作系统版本 Android 标准 WebView 与操作系统的关系 适应安全浏览
Android 7.0 或更高版本 适用于 Android 的 Chrome (Chromium base) 独立 OK
Android 5.0 - 6.0 Android 系统 WebView (Chromium base) 独立 OK
Android 4.4 操作系统嵌入式 WebView (Chromium base) 嵌入式
Android 4.3 或更早版本 操作系统嵌入式 WebView 嵌入式

在 Android 4.3(API 等级 18)之前,未包括安全浏览功能的 WebView 已集成到操作系统中,并在 Android 4.4(API 级别 19)中进行了更改,以便 WebView 包含安全浏览功能。即使如此,也需要注意,因为版本是旧的,并且不支持在应用程序的 WebView 中使用安全浏览功能。

从 Android 5.0(API 等级 21)开始的应用程序中可使用安全浏览功能,该功能在 WebView 与操作系统分离并作为应用程序进行更新时可用。

从 WebView 66 开始,默认情况下启用安全浏览,应用程序端不需要任何特殊设置。但是,如果用户未更新 WebView,或者开发人员的“设置 WebView 实施”选项中的标准 WebView 已从默认值更改,则某些 WebView 版本可能默认情况下不启用安全浏览。因此,如果使用了安全浏览,则必须按如下所示显式启用它。

在 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>

此外,在 Android 8.0(API 等级 26)中添加了多个用于安全浏览的 API。

在 WebSettings 类中添加 setSafeBrowsingEnabled(启用布尔值)是动态启用或禁用每个 WebView 实例的设置方法。在 Android 8.0(API 等级 26)之前,AndroidManifest 中的设置启用或禁用了安全浏览功能,但这只能为应用程序中的所有 WebViews 进行设置。setSafeBrowsingEnabled(启用布尔值)可用于允许为每个 WebView 实例进行动态启用/禁用切换。

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); 
}

此外,在 Android 8.1(API 等级 27)中,添加了用于安全浏览的类和 API。这些功能可用于指定安全浏览初始化过程、访问不安全网页时获取的响应设置、从安全浏览中排除特定站点的白名单设置等。

在 WebView 类中添加的 startSafeBrowsing() 是为应用程序中用于 WebView 的 WebView 组件调用安全浏览初始化过程的方法。初始化结果将传递给第二个参数传递的回调对象,因此,如果初始化失败,并且将 false 传递给回调对象,则建议禁用 WebView 或不加载 URL 等响应。

// 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
        }
    }
});

同样,在 WebView 类中添加的 setSafeBrowsingWhitelist() 是一种以白名单格式设置从安全浏览中排除的主机名和 IP 地址的方法。当将要从安全浏览中排除的主机名和 IP 地址列表作为参数传递时,访问时不会使用安全浏览进行验证。

// 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());
            }
        });

添加到 WebClient 类中的 onSafeBrowsingHit() 是一个回调函数,当确定在启用了安全浏览的 WebView 中访问的 URL 是不安全的网页时,将回调该函数。访问不安全网页的 WebView 的对象被传递到第一个参数,WebResourceRequest 传递到第二个参数,威胁类型传递到第三个参数,而用于在确定页面不安全时设置响应的 SafeBrowsingResponse 对象传递到第四个参数。

使用 SafeBrowsingResponse 对象时的响应可从以下三个选项中选择。

  • backToSafety(布尔值报告):返回上一页而不显示警告(如果没有上一页可用,则显示空白页。)
  • 继续(布尔值报告):忽略警告并显示网页。
  • showInterstitial(boolean allowReporting): 显示警告页(默认响应)

对于 backToSafety() 和 proceed(),参数可用于设置是否向 Google 发送报告,以及为 showInterstitial() 设置参数以显示用于选择是否向 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) {
        callback.showInterstitial(true); // Display warning page with a check box which selects "Send report to Google" or not (Recommended)
        callback.backToSafety(true); // Without displaying warning page, return back to the safe page, and send a report to Google (Recommended)
        callback.proceed(false); // Ignoring the warning, access to the page, and send a report to Google (Not recommended)
    }
}

没有支持这些类和 API 的 Android 支持库可用。因此,要在低于 API 等级 26 或 27 的系统中使用这些类和 API 运行应用程序,必须根据版本或类似措施来分隔进程。

在 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 elable 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
package org.jssec.android.webview.safebrowsing;

import android.support.v7.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
package org.jssec.android.webview.safebrowsing;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
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 suspecious to be a malware site, we are returning back to the safe page.",
		      Toast.LENGTH_LONG).show();
    }
}

4.10.使用通知

Android 提供通知功能,用于向最终用户发送消息。使用通知会导致屏幕上出现一个称为状态栏的区域,您可以在其中显示图标和消息。

_images/image54.png

图 4.10.1 通知示例

Android 5.0(API 等级 21)增强了通知的通信功能,即使屏幕被锁定,也可通过通知显示消息,具体取决于用户和应用程序设置。但是,不正确使用通知会带来以下风险,即仅应向终端用户显示的私有信息可能会被第三方看到。因此,实施此功能时必须谨慎关注隐私和安全。

下表汇总了可见性选项的可能值以及通知的相应行为。

表 4.10.1 通知的可能可见性值和行为
可见性值 通知行为
公共 通知将显示在所有锁定屏幕上。
私有 通知显示在所有锁定的屏幕上;但是,在受密码保护的锁定屏幕上(安全锁定)诸如通知的标题和文本等字段将隐藏(替换为隐藏了私人信息的公开可释放消息)。
秘密 通知不会显示在受密码或其他安全措施(安全锁)保护的锁定屏幕上。(通知显示在不涉及安全锁定的锁定屏幕上。)

4.10.1.示例代码

当通知包含有关终端用户的私人信息时,必须准备并添加一条已排除私人信息的消息,以便在锁定屏幕时显示。

_images/image55.png

图 4.10.2 锁定屏幕上的通知

下面显示了说明如何正确使用包含私人数据的邮件通知的示例代码。

要点:

  1. 在对包含私人数据的消息使用通知时,请准备一个适合公开显示的通知版本(在屏幕锁定时显示)。
  2. 不要在准备公开显示的通知中包括私人信息(屏幕锁定时显示)。
  3. 在创建通知时将可见性明确设置为私人。
  4. 如果可见性设置为私人,则通知可能包含私人信息。
VisibilityPrivateNotificationActivity.java
package org.jssec.notification.visibilityPrivate;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
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;

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

    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 = 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 = 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);

        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.规则手册

创建通知时,必须遵守以下规则。

  1. 无论如何设置可见性,通知都不能包含敏感信息(尽管隐私信息是例外)(必需)
  2. Visibility=Public 的通知不能包含私人信息(必需)
  3. 对于包含私人信息的通知,可见性必须明确设置为“私有”或“机密”(必需)
  4. 在将 Visibility=Private 的通知用于公开显示时,创建一个附加 Visibility=Public 的通知(推荐)

4.10.2.1.无论如何设置可见性,通知都不能包含敏感信息(尽管隐私信息是例外)(必需)

在使用 Android4.3(API 等级 18)或更高版本的终端上,用户可以使用设置窗口授予应用程序读取通知的权限。授予此权限的应用程序将能够读取通知中的所有信息;因此,通知中不得包含敏感信息。(但是,根据可见性设置,通知中可能包括私人信息)。

通知中包含的信息通常不能由发送通知的应用程序之外的应用程序读取。但是,用户可以明确授予某些用户选择的应用程序读取通知中所有信息的权限。由于只有已获得用户权限的应用程序才能读取通知中的信息,因此在通知中包含有关用户的隐私信息没有任何问题。另一方面,如果通知中包括用户的私人信息(例如,仅应用程序开发人员知道的秘密信息)以外的敏感信息,则用户自己可能会尝试读取通知中包含的信息,并可能授予应用程序查看此信息的权限;因此,包括除私人用户信息以外的敏感信息是有问题的。

有关具体方法和条件,请参阅“4.10.3.1.关于向用户授权的查看通知权限”。

4.10.2.2.Visibility=Public 的通知不能包含私密信息(必需)

使用 Visibility=Public 发送通知时,不能在通知中包括私人用户信息。当通知设置为 Visibility=Public 时,即使屏幕被锁定,通知中的信息也会显示。这是因为此类通知存在以下风险:第三方可能在与终端的物理距离内看到和窃取私人信息。

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);

私人信息的典型示例包括发送给用户的电子邮件、用户的位置数据以及“5.5.处理隐私数据”部分列出的其他项目。

4.10.2.3.对于包含私密信息的通知,可见性必须明确设置为“私人”或“机密”(必需)

即使屏幕被锁定,使用 Android 5.0(API 等级 21)或更高版本的终端也将显示通知。因此,当通知包含私人信息时,其可见性标志应明确设置为“私人”或“机密”。这是为了防止锁定屏幕上显示通知中包含的私人信息的风险。

目前,可见性的默认值设置为私有通知,因此只有当此标志明确更改为公共时,才会出现上述风险。但是,可见性的默认值可能会在将来发生变化;因此,为了在处理信息时始终清楚地传达个人意图,必须明确为包含私人信息的通知设置 Visibility=Private。

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.在使用 Visibility=Private 的通知时创建一个附加的通知,其中 Visibility=Public 可用于公共显示(推荐)

当通过 Visibility=Private 的通知进行信息通信时,最好同时创建一个附加通知,用于公共显示;这是为了限制锁定屏幕上显示的信息。

如果公用显示通知未与 Visibility=Private 通知一起注册,则在锁定屏幕时,将显示操作系统准备的默认消息。因此,在这种情况下不存在安全问题。但是,为了在处理信息时始终清楚地传达个人意图,建议明确创建并注册公开显示通知。

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.高级主题

4.10.3.1.关于向用户授权的查看通知权限

如上文 “4.10.2.1.无论如何设置可见性,通知都不能包含敏感信息(尽管隐私信息是例外)(必需)”中所述,在使用 Android 4.3(API 等级 18)或更高版本的终端上,已授予用户权限的某些用户选定应用程序可以读取所有通知中的信息。

_images/image56.png

图 4.10.3访问通知窗口,可从中配置通知读取控件

以下示例代码说明了 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
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);
    }
}

如上所述,通过使用 NotificationListenerService 获取用户权限,可以读取通知。但是,由于通知中包含的信息通常包括终端上的私人信息,因此在处理此类信息时需要小心。

4.11.使用共享内存

以前,Android 操作系统包括共享内存机制,由 android.os.MemoryFile 提供。但是,它不直接提供用于在多个应用程序上共享的 API 或访问控制,并且很难用于一般应用程序。在 Android 8.1(API 等级 27)中,引入了 android.os.SharedMemory 程序包,该程序包使共享存储器机制在一般应用程序中相对容易使用。在 Android 8.1 时,MemoryFile 是 SharedMemory 的包装程序,建议使用 SharedMemory。本节介绍使用此 SharedMemory API 时的重要安全点。

如上所述,此 API 是假定在应用程序服务创建共享内存并将此共享内存提供给其他应用程序时共享提供的应用程序和内存的结构。因此,“4.4.创建/使用服务”中描述的所有信息也适用于提供共享内存的应用程序和使用此共享内存的应用程序。如果您尚未阅读此信息,建议您阅读“4.4.创建/使用服务”,然后继续查看下面的说明。

没有支持 SharedMemory API 的可用 Android 支持库。因此,要在低于 API 等级 27 的系统中使用 SharedMemory 运行应用程序,需要采取措施,例如通过实施等效的虚拟内存机制,例如使用 JNI 包装 C 语言级别 API,并且必须根据版本分隔进程。

4.11.1.Android 共享内存概述

共享内存是一种在多个应用程序之间共享同一物理内存区域的机制。

_images/image96.png

图 4.11.1共享内存概述

上图显示了将共享内存用于应用程序 A 和应用程序 B 时的外观。应用程序 A 创建共享内存对象,并将其提供给应用程序 B。应用程序 A 提供共享内存的角色作为应用程序 A 的服务进行处理。应用程序 B 连接到此服务,请求和获取共享内存,并在共享内存所需的进程完成后,应用程序 B 通知应用程序 A 使用已完成。

例如,如果处理超过正常进程之间允许通信的最大大小 (1 MB)[29]的数据,例如大图像的位图数据,则可以使用共享内存来实现多个进程之间的共享。此外,整个设备使用的内存量可以减少,以实现正常的内存访问,从而实现进程间极高速的通信。但是,由于多个应用程序同时并行访问,因此在某些情况下还必须考虑维护数据的完整性。为避免这种情况,可以在应用程序之间执行独占控制,并且需要进行其他仔细设计,以确保存储器区域正确分割,并且访问区域不会相互干扰。

[29]https://developer.android.com/guide/components/activities/parcelables-and-bundles

如上所述,Android SDK 的共享内存 API 的构建使服务创建共享内存对象并将其提供给其他进程。由于共享内存类 (android.os.SharedMemory) 被定义为可隔离的,因此可以通过绑定器轻松地将共享内存实例传递到其他进程。在随后出现的示例代码中概要介绍服务与客户端之间的交换,其结构如下图所示(这可能因服务的结构而有显著差异)。

_images/image97.png

图 4.11.2 在共享内存服务和客户端之间交换

  • S1.服务使用 SharedMemory.create() 创建共享内存。
  • S2.如果服务本身将使用共享内存,则使用 SharedMemory#map() 将共享内存映射到其自己的内存空间。
  • C1.客户端使用显式意图通过 Context#bindService() 连接到服务。
  • S3.从客户端收到连接请求时,将调用服务的 onBind() 回调。此服务在此阶段执行所需的预处理(如果需要),并返回一个用于连接到客户端的 IBinder。
  • C2.在客户端上,作为 onServiceConnected() 回调的参数返回执行 onBind() 的服务时返回值(IBinder 实例)。然后,此 IBinder 用于执行与服务的通信。
  • C3.客户端请求服务的共享内存。
  • S4.该服务从客户端接收共享内存请求,并在客户端访问共享内存时设置允许的操作(读、写)。
  • S5.服务将共享内存对象传递给客户端。
  • C4.要访问收到的共享内存,客户端会将共享内存映射到其自己的地址空间以供使用。
  • C5, C6.当客户端完成共享内存的使用后,共享内存将从其自己的内存空间取消映射 (C5), 共享内存将关闭 (C6)。
  • C7.然后,客户端通知服务器共享内存的使用已完成。
  • C8.客户端断开与服务的连接。
  • S7.从客户端收到使用完成消息后,服务本身也会取消映射并关闭共享内存。

上面 C2 项中的 onServiceConnected() 定义为一个实施 android.content.ServiceConnection 类的类。有关具体示例,请参阅稍后出现的示例代码。有多种使用 IBinder 的通信方法可用,但示例代码中使用了 Messenger。

4.11.2.示例代码

如前所述,创建共享内存并将其提供给其他应用程序的端作为服务实施。因此,从功能和信息共享的安全性的角度来看,与“4.4.创建/使用服务”中包含的信息没有根本区别。根据4.4.中的分类,下图显示了确定将与谁共享内存的过程。

_images/image98.png

图 4.11.3 用于选择 SharedMemory Service Type 的流程图

表 4.4.2 在“4.4.1.示例代码”中描述了如何实施服务,但对于共享存储器,必须使用活页夹实现与其他应用程序的共享。因此,共享内存不能作为 startService 或 IntentService 服务实施。因此,它的实施如下表所示。

表 4.11.1服务类别和类型(共享内存)
类别 专用服务 公共服务 合作伙伴服务 内部服务
startService 类型 - - - -
IntentService 类型 - - - -
本地绑定类型 OK - - -
Messenger 绑定类型 OK OK - 确定*
AIDL 绑定类型 OK OK OK OK

整体结构与“4.4.1.示例代码”中的结构基本相同。此外,由于特定于共享内存的项目在所有情况下都相同,因此在特定示例代码中,上表中标有星号的项目仅表示适用于内部服务的项目。因此,要在其他情况下使用共享内存,请参阅 “ 4.4.1.1.创建/使用专用服务”到“4.4.1.3.创建/使用合作伙伴服务”中的信息。

4.11.2.1.创建/使用专用服务

在这种情况下,将使用一个结构来共享私有服务在应用程序中包含的多个进程之间创建的共享内存。此外,此私有服务作为独立于应用程序主要进程的进程启动。

要点:

  1. 通过 exported="false" 将创建共享内存的服务显式设置为私有。
  2. 如果应用程序中的某个进程引用由另一个进程写入的数据,则即使该进程是同一应用程序中的进程,也会验证该安全。
  3. 敏感信息可以共享,因为共享内存是同一应用程序中的一个过程。

4.4.1.1.创建/使用专用服务”中的示例代码按意图使用服务,但对于共享内存,不能通过意图共享内存资源,因此必须使用基于本地绑定、消息绑定或 AIDL 绑定的方法。

4.11.2.2.创建/使用公共服务

如 “ 4.4.1.2.创建/使用公共服务”中所述,公共服务是指假定由大量未指定的应用程序使用的服务。因此,还必须假定恶意软件的使用。通常,必须注意 4.4.1.2. 中提到的要点,但从共享内存的角度来看,这些要点会在下面重新讲述。

要点(创建服务):

  1. 使用 exported=“true”显式设置为公共。
  2. 验证请求中包含的参数和数据的安全性,以及启动服务和共享内存的其他操作。
  3. 不得使用共享内存共享敏感信息。

要点(使用服务):

  1. 不得将敏感信息写入共享内存。
  2. 引用由其他应用程序写入的数据时,将验证安全性。

4.11.2.3.创建/使用合作伙伴服务

此信息与“ 4.4.1.3.创建/使用合作伙伴服务”中显示的信息几乎完全相同。但从共享内存的角度来看,这是为了显示以下要点(与4.4.1.3. 中的示例代码类似,这假定使用了 AIDL 绑定服务)

要点(创建服务):

  1. 不要定义意图过滤器,并明确声明 exported=”true”。
  2. 通过预定义的白名单验证请求应用程序的证书。
  3. onBind(onStartCommand, onHandleIntent) 不能用于确定请求者是否为合作伙伴。
  4. 验证已接收意图的安全性,即使该意图是否是从合作伙伴应用程序发送的。
  5. 只有允许向合作伙伴应用程序披露的信息才允许写入共享内存。

要点(使用服务):

  1. 验证请求的合作伙伴服务应用程序的证书是否已在白名单中注册。
  2. 只有允许向请求的合作伙伴应用程序披露的信息才允许写入共享内存。
  3. 使用显式意图呼叫合作伙伴服务。
  4. 即使数据是由合作伙伴应用程序写入的,也要验证数据的安全性。

4.11.2.4.创建/使用内部服务

本节提供了一个示例,其中共享内存由作为公用服务提供,但共享内存仅提供给内部应用程序。如“4.4.1.4.创建/使用内部服务”中的示例,使用 Messenger 绑定服务。4.4.1.4. 中介绍了背景的原理和设置,因此请首先参阅 4.4.1.4.,如果您尚未阅读此信息的话。

服务端应用程序的示例代码(Messenger 绑定)

要点如下所示,但项目 1 至 5 和 7 显示在 “4.4.1.4.创建/使用内部服务”中,项目 6 是特定于共享内存的唯一项目。

要点:

  1. 定义内部签名权限。
  2. 请求内部签名权限的声明。
  3. 不要定义意图过滤器,并明确声明 exported=”true”。
  4. 验证内部签名权限是否由内部应用程序定义。
  5. 验证已接收意向的安全性,即使意图是否是从内部应用程序发送的。
  6. 将共享内存传递到客户端之前,请使用 SharedMemory#setProtect() 限制客户端的可用操作。
  7. 使用与请求应用程序相同的开发人员密钥签署 APK。

为了简化起见,此示例定义了分配共享内存的服务以及在同一应用程序中使用该服务的活动(该服务在同一应用程序中作为单独的进程启动)。因此,签名权限定义和使用声明都包含在清单文件中。

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" />
    <uses-permission
        android:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" />
    <application
        android:allowBackup="true"
        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 -->
	<!-- *** POING 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
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.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.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!";
    private final String greeting2 = "You can write here!";
    private final String greeting3 = "From this point, I'll also write.";
    // 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: 
                    shareWith1(msg);
                    break;
                case MSG_ATTACH2: 
                    shareWith2(msg);
                    break;
                case MSG_DETACH: 
                    unShare(msg);
                    break;
                case MSG_REPLY1: 
                    gotReply(msg);
                    break;
                case 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.请参阅“3.2 谨慎且安全地处理输入数据。”
        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;
        }
        return tBuf;
    }

    // Server side mappings of SharedMemory objects
    private void mapMemory() {
        // mSHMem1: read/write
        m1Buffer1 = mapShared(mSHMem1, PROT_READ | PROT_WRITE, 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
    public 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
    public 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
        m2Buffer1.putInt(greeting2.length());
        m2Buffer1.put(greeting2.getBytes());
        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
    public void unShare(Message msg){
        deAllocateSharedMemory();
    }

    // Accepted invalid message
    public 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
    public 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
    public 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
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 hash values of pkgname and expected preset value
            if (Build.VERSION.SDK_INT >= 28) {
                // * if API Level >= 28, we can validate directly by an API of Package Manager
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
		// else(API Level < 28), by using a facility of PkgCert, get the hash value and compare
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
PkgCert.java
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);
            if (pkginfo.signatures.length != 1) return null;    // Will not handle multiple signatures.
            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();
    }
}

*** 要点 7 *** 导出 APK 时,使用与请求的应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.11.4 使用与请求应用程序相同的开发人员密钥签署 APK

客户端的示例代码

要点:

  1. 声明使用内部签名权限。
  2. 验证内部定义的签名权限是否由内部应用程序定义。
  3. 验证目标应用程序是否由内部证书签名。
  4. 由于目标应用程序是内部应用程序,因此可以发送敏感信息。
  5. 使用显式意图调用内部服务。
  6. 使用与目标应用程序相同的开发人员密钥签署 APK。

此处显示的所有要点与 “4.4.1.4.创建/使用内部服务”中的客户端的要点相同,并且没有特定于共享内存的点。有关使用共享内存的基本要点显示在下面的示例代码中,请参阅以了解更多信息。

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">

    <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="true"
        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 -->
        <!-- For purposes of simplification, make Service which provides SharedMemory to be a difference 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
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;

    // Infomation 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
                    myShared1 = (SharedMemory) msg.obj;
                    useSHMEM1();
                    break;
                case SHMService.SHMEM2: 
		    // SHMEM2 is provided from Service
                    myShared2 = (SharedMemory) msg.obj;
                    useSHMEM2();
                    break;
                case SHMService.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 uswd 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 () {
	// Only read access is permitted for SHMEM1, map it with PROT_READ
        // Mapping with different protection mode will raise an exception
        mBuf1 = mapMemory(SHMService.SHMEM1, 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();
        // 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(SHMService.SHMEM2, 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);
            // 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
    public void alloverNow() {
        // Notify Service that we are done
        sendMessageToService(SHMService.MSG_DETACH);
        // 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();
    }
}

*** 要点 13 *** 导出 APK 时,使用与目标应用程序相同的开发人员密钥签署 APK。

_images/image35.png

图 4.11.5 使用与目标应用程序相同的开发人员密钥签署 APK

4.11.3.规则手册

使用 SharedMemory 时,必须遵守(4.4.2.规则手册中的规则。除了规则手册之外,还必须遵守以下规则。

  1. 提供共享内存的一方正确设置权限,以允许使用方进行访问(必需)
  2. 共享内存中的所有数据均设计为假定共享应用程序会读取该数据(必需)

4.11.3.1.提供共享内存的一方正确设置权限,以允许使用方进行访问(必需)

当共享内存时,在内存中允许的操作设计中,每个应用程序必须将操作限制为防止信息泄漏、更改和损坏所需的最小值。创建 SharedMemory 对象的服务可以使用 SharedMemory#setProtect(),以限制在与其他应用程序共享之前整个共享内存中允许的操作。SharedMemory 对象中允许操作的初始值为读取、写入和执行。除了特殊原因外,应避免使用可执行存储器区域,以防止执行无效代码 [30]。此外,如果其他应用程序需要写入共享内存,则会单独创建并提供特殊用途的共享内存,以便安全共享内存。

[30]对于某些设备(基于所使用的 CPU 架构),如果某个内存区域可读,它将自动变为可执行文件。但是,即使在这些情况下,也可以禁止编写这些区域,以防止其他应用程序在这些区域中编写可执行代码。

SharedMemory#setProtect() 的参数是相应于读取、写入和执行的位标志 (PROT_READ 、 PROT_WRITE 、 PROT_EXEC) 的逻辑 OR。下面显示了一个示例,仅允许对 SharedMemory 对象 shMem 进行读取和写入。

shMem.setProtect(PROT_READ | PROT_WRITE)

必须提前执行 SharedMemory#map(),以便客户端能够访问共享内存中的区域(全部或部分)。在此过程中,可允许的内存操作由参数指定,但不能在服务预先允许的操作之上使用 SharedMemory#setProtect() 指定操作。例如,当服务仅允许读取时,客户端无法指定写入操作。下面显示了一个示例,其中服务执行 map() 时提供的 SharedMemory 对象 ashMem。

ByteBuffer mbuf;
// If the Service only allows READ from ashMem, the following code raises an exception
mbuf = ashMem.map(offset, length, PROT_WRITE);

在客户端,可以调用 setProtect() 来重做设置,以便允许对整个共享内存执行操作,但与 map() 类似,除了服务所允许的操作,不能设置执行其他操作。

4.11.3.2.共享内存中的所有数据均设计为假定共享应用程序会读取该数据(必需)

如上所述,当内存与其他应用程序共享时,服务可以预先设置共享内存的访问权限(读取、写入、执行)。但是,即使将标记设置为 PROT_WRITE 仅允许写入,在某些情况下,也不能禁止读取内存。换言之,如果设备使用的内存管理单元 (MMU) 不支持仅允许写入的内存访问,则允许写入特定的内存区域也允许读取。我们认为大量设备实际上具有此配置,因此,必须在假定共享内存的内容将被其他应用程序识别的前提下执行设计。

// 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, the 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);

虽然可以为标记指定 PROT_NONE 以阻止所有操作,但这会破坏共享内存的用途。

4.11.4.高级主题

4.11.4.1.共享内存的实际状态

到目前为止,我们描述了在多个应用程序之间共享内存的内存共享机制。但是,实际上,共享内存是一种在多个进程之间共享相同物理内存区域的机制。每个进程都会将共享物理内存区域映射到其自己的地址空间,以便访问(这由 SharedMemory#map() 执行)。对于 Android 共享内存,映射的内存区域(对于 Java 语言)是单个 ByteBuffer 对象。(如果分配了超过页面大小的共享内存,通常共享物理内存区域不会划分为连续区域,而是划分为多个非连续页面。但是,如果映射到进程地址空间,则存储器区域将成为连续的地址空间。)

_images/image99.png

图 4.11.6 物理内存和进程地址空间

在基于 Unix 的操作系统中,包括 Android 操作系统,连接的终端、USB 设备或其他外围设备,使用设备文件的概念抽象化,并将设备作为虚拟文件处理。Android 操作系统中的共享内存不是此操作的例外,此处理对应于设备文件 /dev/ashmem。打开此设备文件时,将以与打开普通文件相同的方式返回文件描述符,并通过此过程访问共享内存。与普通文件一样,此文件描述符可以使用 mmap() 映射到进程地址空间。在基于 Unix 的操作系统中,mmap() 是标准系统调用,它可获取各种设备的设备文件描述符,并提供将设备映射到调用进程的地址空间的功能。这也用于 Android 操作系统的共享内存。映射的地址空间以字节序列的形式显示在程序中(如上所述的用于 Java 的 ByteBuffer、C 语言级别的 char*)。

_images/image100.png

图 4.11.7 虚拟文件和地址空间的映射

此框架中进程之间的内存共享等同于共享与此内存区域相应的 /dev/asmem 的文件描述符 [31]。因此,这可降低共享成本,并且在映射到进程的地址空间后,这将实现与正常内存访问相同的效率。

[31]文件描述符是进程中的唯一值,因此当它传递到其他进程时,需要正确转换,但这在 Android SDK API 层面不需要考虑。