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 使用内部帐户”调用。
要点:
- 提供认证器的服务必须是私有的。
- 登录界面的活动必须在验证器应用中实现。
- 登录界面的活动必须实现为公共活动。
- 指定登录界面的活动的类名的显式意图,必须设置为
KEY_INTENT
。
- 敏感信息(如帐户信息或认证令牌)不得输出到日志中。
- 密码不应保存在帐户管理器中。
- 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">
<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
android:name=".AuthenticationService"
android:exported="false" >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<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;
@Override
public void onCreate() {
mAuthenticator = new JssecAuthenticator(this);
}
@Override
public 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;
}
@Override
public 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) {
bundle.putString(AccountManager.KEY_ERROR_CODE, String.valueOf(-1));
bundle.putString(AccountManager.KEY_ERROR_MESSAGE,
mContext.getString(R.string.error_account_exists));
} else {
Intent intent = new Intent(mContext, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
}
return bundle;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle options) throws NetworkErrorException {
Bundle bundle = new Bundle();
if (accountExist(account)) {
Intent intent = new Intent(mContext, LoginActivity.class);
intent.putExtra(RE_AUTH_NAME, account.name);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
} else {
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;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return JSSEC_AUTHTOKEN_LABEL;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public 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;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
requestWindowFeature(Window.FEATURE_LEFT_ICON);
setContentView(R.layout.login_activity);
getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
android.R.drawable.ic_dialog_alert);
mNameEdit = (EditText) findViewById(R.id.username_edit);
mPassEdit = (EditText) findViewById(R.id.password_edit);
mReAuthName = getIntent().getStringExtra(JssecAuthenticator.RE_AUTH_NAME);
if (mReAuthName != null) {
mNameEdit.setText(mReAuthName);
mNameEdit.setInputType(InputType.TYPE_NULL);
mNameEdit.setFocusable(false);
mNameEdit.setEnabled(false);
}
}
public void handleLogin(View view) {
String name = mNameEdit.getText().toString();
String pass = mPassEdit.getText().toString();
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(pass)) {
setResult(RESULT_CANCELED);
finish();
}
WebService web = new WebService();
String authToken = web.login(name, pass);
if (TextUtils.isEmpty(authToken)) {
setResult(RESULT_CANCELED);
finish();
}
Log.i(TAG, "WebService login succeeded");
if (mReAuthName == null) {
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 {
Bundle 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) {
return getAuthToken(username, password);
}
private String getAuthToken(String username, String password) {
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 {
private static final String JSSEC_ACCOUNT_TYPE = "org.jssec.android.accountmanager";
private static final String JSSEC_TOKEN_TYPE = "webservice";
private TextView mLogView;
@Override
public 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");
if (!checkAuthenticator()) return;
AccountManager am = AccountManager.get(this);
am.addAccount(JSSEC_ACCOUNT_TYPE, JSSEC_TOKEN_TYPE, null, null, this,
new AccountManagerCallback<Bundle>() {
@Override
public 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");
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>() {
@Override
public 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.");
}
}
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;
}
private String getTrustedCertificateHash(Context context) {
if (Utils.isDebuggable(context)) {
return "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
} else {
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;
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();
}
}