4.4.1.4 创建/使用内部服务
内部服务是除了内部应用以外的应用禁止使用的服务。 它们用于内部开发的应用,以便安全地共享信息和功能。 以下是使用Messenger绑定类型服务的示例。
要点(创建服务):
定义内部签名权限。
需要内部签名权限。
不要定义意图过滤器,并将导出属性显式设置为
true。确认内部签名权限是由内部应用定义的。
尽管意图是从内部应用发送的,但要小心并安全地处理接收到的意图。
由于请求应用是内部的,因此可以返回敏感信息。
导出 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 --><permissionandroid:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION"android:protectionLevel="signature" /><applicationandroid: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. --><serviceandroid: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 permissionprivate static final String MY_PERMISSION = "org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION";// In-house certificate hash valueprivate 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 listprivate ArrayList<Messenger> mClients = new ArrayList<Messenger>();// Messenger used when service receive data from clientprivate final Messenger mMessenger = new Messenger(new ServiceSideHandler(mClients));// Handler which handles message received from clientprivate static class ServiceSideHandler extends Handler{private ArrayList<Messenger> mClients;public ServiceSideHandler(ArrayList<Messenger> clients){mClients = clients;}@Overridepublic void handleMessage(Message msg){switch(msg.what){case CommonValue.MSG_REGISTER_CLIENT:// Add messenger received from clientmClients.add(msg.replyTo);break;case CommonValue.MSG_UNREGISTER_CLIENT:mClients.remove(msg.replyTo);break;case CommonValue.MSG_SET_VALUE:// Send data to clientsendMessageToClients(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 inhouse.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();}}}public IBinder onBind(Intent intent) {// *** POINT 4 *** Verify that the in-house signature permission is defined by an in-house application.if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {Toast.makeText(this, "In-house defined signature permission is not defined by in-house application.", Toast.LENGTH_LONG).show();return null;}// *** POINT 5 *** Handle the received intent carefully and securely,// even though the intent was sent from an in-house application.// Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely."String param = intent.getStringExtra("PARAM");Toast.makeText(this, String.format("Received parameter ¥"%s¥".", param), Toast.LENGTH_LONG).show();return mMessenger.getBinder();}@Overridepublic void onCreate() {Toast.makeText(this, "Service - onCreate()", Toast.LENGTH_SHORT).show();}@Overridepublic 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;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 Permissionif (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();}}
要点 7:导出 APK 时,请使用与请求应用相同的开发人员密钥对 APK 进行签名。

下面是使用内部服务的活动代码:
要点(使用服务):
声明使用内部签名权限。
确认内部签名权限是由内部应用定义的。
验证目标应用是否使用内部证书签名。
由于目标应用是内部的,因此可以发送敏感信息。
使用显式意图调用内部服务。
即使数据来自内部应用,也要小心并安全地处理收到的结果数据。
导出 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-permissionandroid:name="org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION" /><applicationandroid:icon="@drawable/ic_launcher"android:label="@string/app_name"android:allowBackup="false" ><activityandroid: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 informationprivate 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 permissionprivate static final String MY_PERMISSION = "org.jssec.android.service.inhouseservice.messenger.MY_PERMISSION";// In-house certificate hash valueprivate 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 serviceprivate class ActivitySideHandler extends Handler {@Overridepublic 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 bindSrvice().private ServiceConnection mConnection = new ServiceConnection() {// This is called when the connection with the service has been established.@Overridepublic 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 serviceMessage 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.@Overridepublic void onServiceDisconnected(ComponentName className) {mServiceMessenger = null;Toast.makeText(mContext, "Disconnected from service", Toast.LENGTH_SHORT).show();}};@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.inhouseservice_activity);mContext = this;}// --- StartService control ---public void onStartServiceClick(View v) {// Start bindServicedoBindService();}public void onGetInfoClick(View v) {getServiceinfo();}public void onStopServiceClick(View v) {doUnbindService();}@Overrideprotected 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 informationMessage 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;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 Permissionif (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();}}
要点 14:导出 APK 时,请使用与目标应用相同的开发人员密钥对 APK 进行签名。

