5.3.1 将内部账户添加到账户管理器 示例代码

原书:Android Application Secure Design/Secure Coding Guidebook

译者:飞龙

协议:CC BY-NC-SA 4.0

“5.3.1.1 创建内部帐户”是认证器应用的示例,“5.3.1.2 使用内部帐户”是请求应用的示例。 在 JSSEC 网站上分发的示例代码集中,每个代码集都对应账户管理器的认证器和用户。

5.3.1.1 创建内部账户

以下是认证器应用的示例代码,它使账户管理器能够使用内部帐户。 在此应用中没有可以从主屏幕启动的活动。 请注意,它间接通过账户管理器,从另一个示例代码“5.3.1.2 使用内部帐户”调用。

要点:

  1. 提供认证器的服务必须是私有的。
  2. 登录界面的活动必须在验证器应用中实现。
  3. 登录界面的活动必须实现为公共活动。
  4. 指定登录界面的活动的类名的显式意图,必须设置为KEY_INTENT
  5. 敏感信息(如帐户信息或认证令牌)不得输出到日志中。
  6. 密码不应保存在帐户管理器中。
  7. HTTPS 应该用于认证器与在线服务之间的通信。

提供认证器的账户管理器 IBinder 的服务,在AndroidManifest.xml中定义。 通过元数据指定编写认证器的资源XML文件。

账户管理器认证器/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="org.jssec.android.accountmanager.authenticator"xmlns:tools="http://schemas.android.com/tools"><!-- Necessary Permission to implement Authenticator --><uses-permission android:name="android.permission.GET_ACCOUNTS" /><uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /><application
        android:allowBackup="false"android:icon="@drawable/ic_launcher"android:label="@string/app_name" ><!-- Service which gives IBinder of Authenticator to AccountManager --><!-- *** POINT 1 *** The service that provides an authenticator must be private. --><service
            android:name=".AuthenticationService"android:exported="false" ><!-- intent-filter and meta-data are usual pattern. --><intent-filter><action android:name="android.accounts.AccountAuthenticator" /></intent-filter><meta-data
            android:name="android.accounts.AccountAuthenticator"android:resource="@xml/authenticator" /></service><!-- Activity for for login screen which is displayed when adding an account --><!-- *** POINT 2 *** The login screen activity must be implemented in an authenticator application. --><!-- *** POINT 3 *** The login screen activity must be made as a public activity. --><activity
            android:name=".LoginActivity"android:exported="true"android:label="@string/login_activity_title"android:theme="@android:style/Theme.Dialog"tools:ignore="ExportedActivity" /></application>
</manifest>

通过 XML 文件定义认证器,指定内部账户的账户类型以及其他。

res/xml/authenticator.xml

<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"android:accountType="org.jssec.android.accountmanager"android:icon="@drawable/ic_launcher"android:label="@string/label"android:smallIcon="@drawable/ic_launcher"android:customTokens="true" />

AccountManager提供Authenticator实例的服务。 简单的实现返回JssecAuthenticator类的实例,它就是由onBind()在此示例中实现的Authenticator,这就足够了。

AuthenticationService.java

package org.jssec.android.accountmanager.authenticator;import android.app.Service;
import android.content.Intent;
import android.os.IBinder;public class AuthenticationService extends Service {private JssecAuthenticator mAuthenticator;@Overridepublic void onCreate() {mAuthenticator = new JssecAuthenticator(this);}@Overridepublic IBinder onBind(Intent intent) {return mAuthenticator.getIBinder();}
}

JssecAuthenticator是在此示例中实现的认证器。 它继承了AbstractAccountAuthenticator,并且实现了所有的抽象方法。 这些方法由账户管理器调用。 在addAccount()getAuthToken()中,用于启动LoginActivity,从在线服务中获取认证令牌的意图返回到账户管理器。

JssecAuthenticator.java

package org.jssec.android.accountmanager.authenticator;import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;public class JssecAuthenticator extends AbstractAccountAuthenticator {public static final String JSSEC_ACCOUNT_TYPE = "org.jssec.android.accountmanager";public static final String JSSEC_AUTHTOKEN_TYPE = "webservice";public static final String JSSEC_AUTHTOKEN_LABEL = "JSSEC Web Service";public static final String RE_AUTH_NAME = "reauth_name";protected final Context mContext;public JssecAuthenticator(Context context) {super(context);mContext = context;}@Overridepublic Bundle addAccount(AccountAuthenticatorResponse response, String accountType,String authTokenType, String[] requiredFeatures, Bundle options)throws NetworkErrorException {AccountManager am = AccountManager.get(mContext);Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);Bundle bundle = new Bundle();if (accounts.length > 0) {// In this sample code, when an account already exists, consider it as an error.bundle.putString(AccountManager.KEY_ERROR_CODE, String.valueOf(-1));bundle.putString(AccountManager.KEY_ERROR_MESSAGE,mContext.getString(R.string.error_account_exists));} else {// *** POINT 2 *** The login screen activity must be implemented in an authenticator application.// *** POINT 4 *** The explicit intent which the class name of the login screen activity is specified must be set to KEY_INTENT.Intent intent = new Intent(mContext, LoginActivity.class);intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);bundle.putParcelable(AccountManager.KEY_INTENT, intent);}return bundle;}@Overridepublic Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,String authTokenType, Bundle options) throws NetworkErrorException {Bundle bundle = new Bundle();if (accountExist(account)) {// *** POINT 4 *** KEY_INTENT must be given an explicit intent that is specified the class name of the login screen activity.Intent intent = new Intent(mContext, LoginActivity.class);intent.putExtra(RE_AUTH_NAME, account.name);bundle.putParcelable(AccountManager.KEY_INTENT, intent);} else {// When the specified account doesn't exist, consider it as an error.bundle.putString(AccountManager.KEY_ERROR_CODE, String.valueOf(-2));bundle.putString(AccountManager.KEY_ERROR_MESSAGE,mContext.getString(R.string.error_account_not_exists));}return bundle;}@Overridepublic String getAuthTokenLabel(String authTokenType) {return JSSEC_AUTHTOKEN_LABEL;}@Overridepublic Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,Bundle options) throws NetworkErrorException {return null;}@Overridepublic Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {return null;}@Overridepublic Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,String authTokenType, Bundle options) throws NetworkErrorException {return null;}@Overridepublic Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,String[] features) throws NetworkErrorException {Bundle result = new Bundle();result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);return result;}private boolean accountExist(Account account) {AccountManager am = AccountManager.get(mContext);Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);for (Account ac : accounts) {if (ac.equals(account)) {return true;}}return false;}
}

这是登录活动,它向在线服务发送帐户名称和密码,并执行登录认证,并因此获得认证令牌。 它会在添加新帐户或再次获取认证令牌时显示。 假设在线服务的实际访问在WebService类中实现。

LoginActivity.java

package org.jssec.android.accountmanager.authenticator;import org.jssec.android.accountmanager.webservice.WebService;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.EditText;public class LoginActivity extends AccountAuthenticatorActivity {private static final String TAG = AccountAuthenticatorActivity.class.getSimpleName();private String mReAuthName = null;private EditText mNameEdit = null;private EditText mPassEdit = null;@Overridepublic void onCreate(Bundle icicle) {super.onCreate(icicle);// Display alert iconrequestWindowFeature(Window.FEATURE_LEFT_ICON);setContentView(R.layout.login_activity);getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,android.R.drawable.ic_dialog_alert);// Find a widget in advancemNameEdit = (EditText) findViewById(R.id.username_edit);mPassEdit = (EditText) findViewById(R.id.password_edit);// *** POINT 3 *** The login screen activity must be made as a public activity, and suppose the attack access from other application.// Regarding external input, only RE_AUTH_NAME which is String type of Intent#extras, are handled.// This external input String is passed toextEdit#setText(), WebService#login(),new Account(),// as a parameter,it's verified that there's no problem if any character string is passed.mReAuthName = getIntent().getStringExtra(JssecAuthenticator.RE_AUTH_NAME);if (mReAuthName != null) {// Since LoginActivity is called with the specified user name, user name should not be editable.mNameEdit.setText(mReAuthName);mNameEdit.setInputType(InputType.TYPE_NULL);mNameEdit.setFocusable(false);mNameEdit.setEnabled(false);}}// It's executed when login button is pressed.public void handleLogin(View view) {String name = mNameEdit.getText().toString();String pass = mPassEdit.getText().toString();if (TextUtils.isEmpty(name) || TextUtils.isEmpty(pass)) {// Process when the inputed value is incorrectsetResult(RESULT_CANCELED);finish();}// Login to online service based on the inpputted account information.WebService web = new WebService();String authToken = web.login(name, pass);if (TextUtils.isEmpty(authToken)) {// Process when authentication failedsetResult(RESULT_CANCELED);finish();}// Process when login was successful, is as per below.// *** POINT 5 *** Sensitive information (like account information or authentication token) must not be output to the log.Log.i(TAG, "WebService login succeeded");if (mReAuthName == null) {// Register accounts which logged in successfully, to aAccountManager// *** POINT 6 *** Password should not be saved in Account Manager.AccountManager am = AccountManager.get(this);Account account = new Account(name, JssecAuthenticator.JSSEC_ACCOUNT_TYPE);am.addAccountExplicitly(account, null, null);am.setAuthToken(account, JssecAuthenticator.JSSEC_AUTHTOKEN_TYPE, authToken);Intent intent = new Intent();intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, name);intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE,JssecAuthenticator.JSSEC_ACCOUNT_TYPE);setAccountAuthenticatorResult(intent.getExtras());setResult(RESULT_OK, intent);} else {// Return authentication tokenBundle bundle = new Bundle();bundle.putString(AccountManager.KEY_ACCOUNT_NAME, name);bundle.putString(AccountManager.KEY_ACCOUNT_TYPE,JssecAuthenticator.JSSEC_ACCOUNT_TYPE);bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);setAccountAuthenticatorResult(bundle);setResult(RESULT_OK);}finish();}
}

实际上,WebService类在这里是虚拟实现,这是假设认证总是成功的示例实现,并且固定字符串作为认证令牌返回。

WebService.java

package org.jssec.android.accountmanager.webservice;
public class WebService {/*** Suppose to access to account managemnet function of online service.** @param username Account name character string* @param password password character string* @return Return authentication token*/public String login(String username, String password) {// *** POINT 7 *** HTTPS should be used for communication between an authenticator and the online services.// Actually, communication process with servers is implemented here, but Omit here, since this is a sample.return getAuthToken(username, password);}private String getAuthToken(String username, String password) {// In fact, get the value which uniqueness and impossibility of speculation are guaranteed by the server,// but the fixed value is returned without communication here, since this is sample.return "c2f981bda5f34f90c0419e171f60f45c";}
}

5.3.1.2 使用内部账户

以下是应用示例代码,它添加内部帐户并获取认证令牌。 当另一个示例应用“5.3.1.1 创建内部帐户”安装在设备上时,可以添加内部帐户或获取认证令牌。 仅当两个应用的签名密钥不同时,才会显示“访问请求”界面。

要点:

在验证认证器是否正常之后,执行账户流程。

AccountManager用户应用的AndroidManifest.xml。 声明使用必要的权限。请参阅“5.3.3.1 账户管理器和权限的使用”来了解必要的权限。

账户管理器用户/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="org.jssec.android.accountmanager.user" ><uses-permission android:name="android.permission.GET_ACCOUNTS" /><uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /><uses-permission android:name="android.permission.USE_CREDENTIALS" /><application
        android:allowBackup="false"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activity
            android:name=".UserActivity"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>

用户应用的活动。 当点击屏幕上的按钮时,会执行addAccount()getAuthToken()。 在某些情况下,对应特定帐户类型的认证器可能是伪造的,因此请注意在验证认证器正常后,启动帐户流程。

UserActivity.java

package org.jssec.android.accountmanager.user;import java.io.IOException;
import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorDescription;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;public class UserActivity extends Activity {// Information of the Authenticator to be usedprivate static final String JSSEC_ACCOUNT_TYPE = "org.jssec.android.accountmanager";private static final String JSSEC_TOKEN_TYPE = "webservice";private TextView mLogView;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.user_activity);mLogView = (TextView)findViewById(R.id.logview);}public void addAccount(View view) {logLine();logLine("Add a new account");// *** POINT 1 *** Execute the account process after verifying if the authenticator is regular one.if (!checkAuthenticator()) return;AccountManager am = AccountManager.get(this);am.addAccount(JSSEC_ACCOUNT_TYPE, JSSEC_TOKEN_TYPE, null, null, this,new AccountManagerCallback<Bundle>() {@Overridepublic void run(AccountManagerFuture<Bundle> future) {try {Bundle result = future.getResult();String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE);String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);if (type != null && name != null) {logLine("Add the following accounts:");logLine(" Account type: %s", type);logLine(" Account name: %s", name);} else {String code = result.getString(AccountManager.KEY_ERROR_CODE);String msg = result.getString(AccountManager.KEY_ERROR_MESSAGE);logLine("The account cannot be added");logLine(" Error code %s: %s", code, msg);}} catch (OperationCanceledException e) {} catch (AuthenticatorException e) {} catch (IOException e) {}}}, null);}public void getAuthToken(View view) {logLine();logLine("Get token");// *** POINT 1 *** After checking that the Authenticator is the regular one, execute account process.if (!checkAuthenticator()) return;AccountManager am = AccountManager.get(this);Account[] accounts = am.getAccountsByType(JSSEC_ACCOUNT_TYPE);if (accounts.length > 0) {Account account = accounts[0];am.getAuthToken(account, JSSEC_TOKEN_TYPE, null, this,new AccountManagerCallback<Bundle>() {@Overridepublic void run(AccountManagerFuture<Bundle> future) {try {Bundle result = future.getResult();String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);String authtoken = result.getString(AccountManager.KEY_AUTHTOKEN);logLine("%s-san's token:", name);if (authtoken != null) {logLine(" %s", authtoken);} else {logLine(" Couldn't get");}} catch (OperationCanceledException e) {logLine(" Exception: %s",e.getClass().getName());} catch (AuthenticatorException e) {logLine(" Exception: %s",e.getClass().getName());} catch (IOException e) {logLine(" Exception: %s",e.getClass().getName());}}}, null);} else {logLine("Account is not registered.");}}// *** POINT 1 *** Verify that Authenticator is regular one.private boolean checkAuthenticator() {AccountManager am = AccountManager.get(this);String pkgname = null;for (AuthenticatorDescription ad : am.getAuthenticatorTypes()) {if (JSSEC_ACCOUNT_TYPE.equals(ad.type)) {pkgname = ad.packageName;break;}}if (pkgname == null) {logLine("Authenticator cannot be found.");return false;}logLine(" Account type: %s", JSSEC_ACCOUNT_TYPE);logLine(" Package name of Authenticator: ");logLine(" %s", pkgname);if (!PkgCert.test(this, pkgname, getTrustedCertificateHash(this))) {logLine(" It's not regular Authenticator(certificate is not matched.)");return false;}logLine(" This is regular Authenticator.");return true;}// Certificate hash value of regular Authenticator application// Certificate hash value can be checked in sample applciation JSSEC CertHash Checkerprivate String getTrustedCertificateHash(Context context) {if (Utils.isDebuggable(context)) {// Certificate hash value of debug.keystore "androiddebugkey"return "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";} else {// Certificate hash value of keystore "my company key"return "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";}}private void log(String str) {mLogView.append(str);}private void logLine(String line) {log(line + "¥n");}private void logLine(String fmt, Object... args) {logLine(String.format(fmt, args));}private void logLine() {log("¥n");}
}

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

安卓应用安全指南 5.3.1 将内部账户添加到账户管理器 示例代码相关推荐

  1. 安卓应用安全指南 5.3.3 将内部账户添加到账户管理器 高级话题

    5.3.3 将内部账户添加到账户管理器 高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY ...

  2. 安卓应用安全指南 5.3.2 将内部账户添加到账户管理器 规则书

    5.3.2 将内部账户添加到账户管理器 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY- ...

  3. 安卓应用安全指南 4.5.1 使用 SQLite 示例代码

    安卓应用安全指南 4.5.1 使用 SQLite 示例代码 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议: ...

  4. 安卓应用安全指南 4.4.3 创建/使用服务高级话题

    安卓应用安全指南 4.4.3 创建/使用服务高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC ...

  5. 安卓应用安全指南 4.6.3 处理文件 高级话题

    安卓应用安全指南 4.6.3 处理文件 高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY ...

  6. 安卓应用安全指南 4.6.2 处理文件 规则书

    安卓应用安全指南 4.6.2 处理文件 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY- ...

  7. 安卓应用安全指南 4.6.1 处理文件 示例代码

    安卓应用安全指南 4.6.1 处理文件 示例代码 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY ...

  8. 安卓应用安全指南 4.5.2 使用 SQLite 规则书

    安卓应用安全指南 4.5.2 使用 SQLite 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:C ...

  9. 安卓应用安全指南 4.4.2 创建/使用服务 规则书

    安卓应用安全指南 4.4.2 创建/使用服务 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC ...

最新文章

  1. 限制系统扩展能力的瓶颈有哪些?
  2. java代码段替换,java-片段添加或替换不起作用
  3. VC中栈溢出/Stack overflow怎么办?
  4. 南邮计算机图形学水不水,南邮计算机图形学实验报告(修正版)….doc
  5. 廖大python实战项目第三天
  6. 数学,原来可以这么美!
  7. ---------愿 青春与我皆不付---------------------
  8. VUE 项目中引入 json 配置
  9. php 查找数组相同元素,查找数组中重复的元素
  10. STM32之FSMC-SRAM/NOR原理
  11. sourceInsight4 破解笔记(完美破解)【转】
  12. Mybatis的直接执行SQL
  13. 初次涉足手机广告联盟行业
  14. 《活着》:永远不要相信苦难是值得的,​苦难就是苦难
  15. ceph 写流程(1)
  16. 【PS/PSD】237款日系小清新文艺唯美梦幻手绘插画分层PSD素材
  17. 云服务器几核CPU几G内存几M带宽够用
  18. [论文阅读] LCC-NLM(局部颜色校正, 非线性mask)
  19. 二叉树遍历【递归非递归】
  20. 【ATF】庄卓然(南天):掌上精彩-连接过去与未来

热门文章

  1. Qt创建多线程的两种方法
  2. STM32F103_RGB彩灯
  3. html程序国庆节祝福,2018最新的国庆节祝福语
  4. mysql报错注入实战_手工注入——MySQL手工注入实战和分析
  5. STM32学习——USART收发数据
  6. Linux netfilter源码分析(2)
  7. android 读写文件 简书,Android10 文件操作适配
  8. 已创建仓库后 github提交流程
  9. JavaScript-面试 表单验证
  10. IO、NIO、AIO