安卓应用安全指南 4.2.1 创建/使用广播接收器 示例代码
4.2.1 创建/使用广播接收器 示例代码
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
接收广播需要创建广播接收器。 使用广播接收器的风险和对策,根据收到的广播的类型而有所不同。 你可以在以下判断流程中找到你的广播接收器。 接收应用无法检查发送广播的应用的包名称,它是链接伙伴所需的。 因此,无法创建用于伙伴的广播接收器。
表 4.2:广播接收器的类型定义:
类型 | 定义 |
---|---|
私有 | 只能接收来自相同应用的广播的广播接收器,所以是最安全的 |
公共 | 可以接收来自未指定的大量应用的广播的广播接收器 |
内部 | 只能接收来自其他内部应用的广播的广播接收器 |
另外,根据定义方法,广播接收器可以分为两类:静态和动态。 它们之间的差异可以在下图中找到。 示例代码展示了每类的实现方法。 还描述了发送应用的实现方法,因为发送信息的对策取决于接收器来确定。
表 4.2-2
定义方法 | 特性 |
---|---|
静态 | 由AndroidManifest.xml 中的<receiver> 元素定义 |
动态 | 通过在程序中调用registerReceiver() 和unregisterReceiver() ,动态注册和注销广播接收器 |
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. Please refer to "3.2 Handling Input Data Carefully and Securely."
String param = intent.getStringExtra("PARAM");
Toast.makeText(context,
String.format("Received param: ¥"%s¥"", param),
Toast.LENGTH_SHORT).show();
// *** POINT 3 *** Sensitive information can be sent as the returned results since the requests come from within the same application.
setResultCode(Activity.RESULT_OK);
setResultData("Sensitive Info from Receiver");
abortBroadcast();
}
}
向私有广播接收器发送广播的代码展示在下面:
要点(发送广播):
4) 使用带有指定类的显式意图,来调用相同应用中的接收器。
5) 敏感信息可以发送,因为目标接收器在相同应用中。
6) 小心并安全地处理收到的返回结果,即使数据来自相同应用中的接收器。
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. Please refer to "3.2 Handling Input Data Carefully and Securely."
String data = getResultData();
PrivateSenderActivity.this.logLine(
String.format("Received result: ¥"%s¥"", data));
}
};
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
4.2.1.2 公共广播接收器
公共广播接收器是可以从未指定的大量应用程序接收广播的广播接收器,因此有必要注意,它可能从恶意软件接收广播。
要点(接收广播):
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. Please refer to "3.2 Handling Input Data Carefully and Securely."
if (MY_BROADCAST_PUBLIC.equals(intent.getAction())) {
String param = intent.getStringExtra("PARAM");
Toast.makeText(context,
String.format("%s:¥nReceived param: ¥"%s¥"", getName(), param),
Toast.LENGTH_SHORT).show();
}
// *** POINT 3 *** When returning a result, do not include sensitive information.
// Since this is a public broadcast receiver, the requesting application may be malware.
// If no problem when the information is taken by malware, it can be returned as result.
setResultCode(Activity.RESULT_OK);
setResultData(String.format("Not Sensitive Info from %s", getName()));
abortBroadcast();
}
}
静态广播接收器定义在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.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);
}
}
接下来,展示了将广播发送到公共广播接收器的示例代码。 当向公共广播接收器发送广播时,需要注意广播可以被恶意软件接收。
要点(发送广播):
4) 不要发送敏感信息
5) 接受广播时,小心并安全地处理结果数据
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. Please refer to "3.2 Handling Input Data Carefully and Securely."
String data = getResultData();
PublicSenderActivity.this.logLine(
String.format("Received result: ¥"%s¥"", data));
}
};
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
4.2.1.3 内部广播接收器
内部广播接收器是广播接收器,它将永远不会收到从内部应用以外发送的任何广播。 它由几个内部应用组成,用于保护内部应用处理的信息或功能。
要点(接收广播):
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 进行签名。
下面,展示了用于向内部广播接收器发送广播的示例代码。
要点(发送广播):
10) 定义内部签名权限来接收结果。
11) 声明使用内部签名权限来接收广播。
12) 确认内部签名权限是由内部应用定义的。
13) 由于请求应用是内部应用,因此可以返回敏感信息。
14) 需要接收器的内部签名权限。
15) 小心并安全地处理收到的结果数据。
16) 导出 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 进行签名。