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. Please refer to "3.2 Handling Input Data Carefully and Securely."
        if (MY_BROADCAST_INHOUSE.equals(intent.getAction())) {
            String param = intent.getStringExtra("PARAM");
            Toast.makeText(context,
            String.format("%s:¥nReceived param: ¥"%s¥"", getName(), param),
            Toast.LENGTH_SHORT).show();
        }
        // *** POINT 8 *** Sensitive information can be returned since the requesting application is inhouse.
        setResultCode(Activity.RESULT_OK);
        setResultData(String.format("Sensitive Info from %s", getName()));
        abortBroadcast();
    }
}

静态广播接收器定义在AndroidManifest.xml中。

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()来执行注册/注销。 为了通过按钮操作执行注册/注销,该按钮PublicReceiverActivity中定义。 由于动态广播接收器实例的作用域比PublicReceiverActivity长,因此不能将其保存为PublicReceiverActivity的成员变量。 在这种情况下,请将动态广播接收器实例保存为DynamicReceiverService的成员变量,然后从PublicReceiverActivity启动/结束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;

public class SigPerm {
    
    public static boolean test(Context ctx, String sigPermName, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, sigPermName));
    }
    
    public static String hash(Context ctx, String sigPermName) {
        if (sigPermName == null) return null;
        try {
            // Get the package name of the application which declares a permission named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi;
            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 null;
            // Return the certificate hash value of the application which declares a permission named sigPermName.
            return PkgCert.hash(ctx, pkgname);
        } catch (NameNotFoundException e) {
            return null;
        }
    }
}

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

导出 APK 时,使用与发送应用相同的开发人员密钥对 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. Please refer to "3.2 Handling Input Data Carefully and Securely."
            String data = getResultData();
            InhouseSenderActivity.this.logLine(String.format("Received result: ¥"%s¥"", data));
        }
    };
    
    private TextView mLogView;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }
    
    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("¥n");
    }
}

SigPerm.java

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;

public class SigPerm {

    public static boolean test(Context ctx, String sigPermName, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, sigPermName));
    }
    
    public static String hash(Context ctx, String sigPermName) {
        if (sigPermName == null) return null;
        try {
            // Get the package name of the application which declares a permission named sigPermName.
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi;
            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 null;
            // Return the certificate hash value of the application which declares a permission named sigPermName.
            return PkgCert.hash(ctx, pkgname);
        } catch (NameNotFoundException e) {
            return null;
        }
    }
}

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

导出 APK 时,使用与发送应用相同的开发人员密钥对 APK 进行签名。


书籍推荐