blob: 7ce176fe27a74f8641ff0b60ef182c134959fe91 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.keychain;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.AccountsException;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.security.Credentials;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.security.KeyStore;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charsets;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import javax.security.auth.x500.X500Principal;
import libcore.io.Base64;
import org.apache.harmony.xnet.provider.jsse.TrustedCertificateStore;
public class KeyChainService extends Service {
private static final String TAG = "KeyChainService";
private AccountManager mAccountManager;
private final Object mAccountLock = new Object();
private Account mAccount;
@Override public void onCreate() {
super.onCreate();
mAccountManager = AccountManager.get(this);
}
private final IKeyChainService.Stub mIKeyChainService = new IKeyChainService.Stub() {
private final KeyStore mKeyStore = KeyStore.getInstance();
private final TrustedCertificateStore mTrustedCertificateStore
= new TrustedCertificateStore();
@Override public byte[] getPrivateKey(String alias, String authToken) {
return getKeyStoreEntry(Credentials.USER_PRIVATE_KEY, alias, authToken);
}
@Override public byte[] getCertificate(String alias, String authToken) {
return getKeyStoreEntry(Credentials.USER_CERTIFICATE, alias, authToken);
}
private byte[] getKeyStoreEntry(String type, String alias, String authToken) {
if (alias == null) {
throw new NullPointerException("alias == null");
}
if (authToken == null) {
throw new NullPointerException("authtoken == null");
}
if (!isKeyStoreUnlocked()) {
throw new IllegalStateException("keystore locked");
}
String peekedAuthToken = mAccountManager.peekAuthToken(mAccount, alias);
if (peekedAuthToken == null) {
throw new IllegalStateException("peekedAuthToken == null");
}
if (!peekedAuthToken.equals(authToken)) {
throw new IllegalStateException("authtoken mismatch");
}
String key = type + alias;
byte[] bytes = mKeyStore.get(key);
if (bytes == null) {
return null;
}
return bytes;
}
private boolean isKeyStoreUnlocked() {
return (mKeyStore.state() == KeyStore.State.UNLOCKED);
}
@Override public void installCaCertificate(byte[] caCertificate) {
// only the CertInstaller should be able to add new trusted CAs
final String expectedPackage = "com.android.certinstaller";
final String actualPackage = getPackageManager().getNameForUid(getCallingUid());
if (!expectedPackage.equals(actualPackage)) {
throw new IllegalStateException(actualPackage);
}
try {
synchronized (mTrustedCertificateStore) {
mTrustedCertificateStore.installCertificate(parseCertificate(caCertificate));
}
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (CertificateException e) {
throw new IllegalStateException(e);
}
}
private X509Certificate parseCertificate(byte[] bytes) throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
}
@Override public boolean reset() {
// only Settings should be able to reset
final String expectedPackage = "android.uid.system:1000";
final String actualPackage = getPackageManager().getNameForUid(getCallingUid());
if (!expectedPackage.equals(actualPackage)) {
throw new IllegalStateException(actualPackage);
}
boolean ok = true;
synchronized (mAccountLock) {
// remote Accounts from AccountManager to revoke any
// granted credential grants to applications
Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
for (Account a : accounts) {
try {
if (!mAccountManager.removeAccount(a, null, null).getResult()) {
ok = false;
}
} catch (AccountsException e) {
Log.w(TAG, "Problem removing account " + a, e);
ok = false;
} catch (IOException e) {
Log.w(TAG, "Problem removing account " + a, e);
ok = false;
}
}
}
synchronized (mTrustedCertificateStore) {
// delete user-installed CA certs
for (String alias : mTrustedCertificateStore.aliases()) {
if (TrustedCertificateStore.isUser(alias)) {
try {
mTrustedCertificateStore.deleteCertificateEntry(alias);
} catch (IOException e) {
Log.w(TAG, "Problem removing CA certificate " + alias, e);
ok = false;
} catch (CertificateException e) {
Log.w(TAG, "Problem removing CA certificate " + alias, e);
ok = false;
}
}
}
return ok;
}
}
};
private class KeyChainAccountAuthenticator extends AbstractAccountAuthenticator {
/**
* 264 was picked becuase it is the length in bytes of Google
* authtokens which seems sufficiently long and guaranteed to
* be storable by AccountManager.
*/
private final int AUTHTOKEN_LENGTH = 264;
private final SecureRandom mSecureRandom = new SecureRandom();
private KeyChainAccountAuthenticator(Context context) {
super(context);
}
@Override public Bundle editProperties(AccountAuthenticatorResponse response,
String accountType) {
return null;
}
@Override public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType,
String authTokenType,
String[] requiredFeatures,
Bundle options) {
return null;
}
@Override public Bundle confirmCredentials(AccountAuthenticatorResponse response,
Account account,
Bundle options) {
return null;
}
/**
* Called on an AccountManager cache miss, so generate a new value.
*/
@Override public Bundle getAuthToken(AccountAuthenticatorResponse response,
Account account,
String authTokenType,
Bundle options) {
byte[] bytes = new byte[AUTHTOKEN_LENGTH];
mSecureRandom.nextBytes(bytes);
String authToken = Base64.encode(bytes);
Bundle bundle = new Bundle();
bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, KeyChain.ACCOUNT_TYPE);
bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return bundle;
}
@Override public String getAuthTokenLabel(String authTokenType) {
// return authTokenType unchanged, it was a user specified
// alias name, doesn't need to be localized
return authTokenType;
}
@Override public Bundle updateCredentials(AccountAuthenticatorResponse response,
Account account,
String authTokenType,
Bundle options) {
return null;
}
@Override public Bundle hasFeatures(AccountAuthenticatorResponse response,
Account account,
String[] features) {
return null;
}
};
private final IBinder mAuthenticator = new KeyChainAccountAuthenticator(this).getIBinder();
@Override public IBinder onBind(Intent intent) {
if (IKeyChainService.class.getName().equals(intent.getAction())) {
// ensure singleton keychain account exists
synchronized (mAccountLock) {
Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
if (accounts.length == 0) {
// TODO localize account name
mAccount = new Account("Android Key Chain", KeyChain.ACCOUNT_TYPE);
mAccountManager.addAccountExplicitly(mAccount, null, null);
} else if (accounts.length == 1) {
mAccount = accounts[0];
} else {
throw new IllegalStateException();
}
}
return mIKeyChainService;
}
if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
return mAuthenticator;
}
return null;
}
}