Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b9a07c18 authored by Brian Carlstrom's avatar Brian Carlstrom
Browse files

Adding KeyChain API and IKeyChainService

Change-Id: Id3eaa2d1315481f199777b50e875811e3532988a
parent f76dc56c
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -158,6 +158,7 @@ LOCAL_SRC_FILES += \
	core/java/com/android/internal/view/IInputMethodSession.aidl \
	core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \
	core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl \
	keystore/java/android/security/IKeyChainService.aidl \
	location/java/android/location/ICountryDetector.aidl \
	location/java/android/location/ICountryListener.aidl \
	location/java/android/location/IGeocodeProvider.aidl \
+31 −0
Original line number Diff line number Diff line
/*
 * 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 android.security;

import android.os.Bundle;

/**
 * Caller is required to ensure that {@link KeyStore#unlock
 * KeyStore.unlock} was successful.
 *
 * @hide
 */
interface IKeyChainService {
    byte[] getPrivate(String alias, String authToken);
    byte[] getCertificate(String alias, String authToken);
    byte[] getCaCertificate(String alias, String authToken);
    String findIssuer(in Bundle cert);
}
+372 −0
Original line number Diff line number Diff line
/*
 * 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 android.security;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import dalvik.system.CloseGuard;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters;
import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;

/**
 * @hide
 */
public final class KeyChain {

    private static final String TAG = "KeyChain";

    /**
     * @hide Also used by KeyChainService implementation
     */
    public static final String ACCOUNT_TYPE = "com.android.keychain";

    /**
     * @hide Also used by KeyChainService implementation
     */
    // TODO This non-localized CA string to be removed when CAs moved out of keystore
    public static final String CA_SUFFIX = " CA";

    public static final String KEY_INTENT = "intent";

    /**
     * Intentionally not public to leave open the future possibility
     * of hardware based keys. Callers should use {@link #toPrivateKey
     * toPrivateKey} in order to convert a bundle to a {@code
     * PrivateKey}
     */
    private static final String KEY_PKCS8 = "pkcs8";

    /**
     * Intentionally not public to leave open the future possibility
     * of hardware based certs. Callers should use {@link
     * #toCertificate toCertificate} in order to convert a bundle to a
     * {@code PrivateKey}
     */
    private static final String KEY_X509 = "x509";

    /**
     * Returns an {@code Intent} for use with {@link
     * android.app.Activity#startActivityForResult
     * startActivityForResult}. The result will be returned via {@link
     * android.app.Activity#onActivityResult onActivityResult} with
     * {@link android.app.Activity#RESULT_OK RESULT_OK} and the alias
     * in the returned {@code Intent}'s extra data with key {@link
     * android.content.Intent#EXTRA_TEXT Intent.EXTRA_TEXT}.
     */
    public static Intent chooseAlias() {
        return new Intent("com.android.keychain.CHOOSER");
    }

    /**
     * Returns a new {@code KeyChain} instance. When the caller is
     * done using the {@code KeyChain}, it must be closed with {@link
     * #close()} or resource leaks will occur.
     */
    public static KeyChain getInstance(Context context) throws InterruptedException {
        return new KeyChain(context);
    }

    private final AccountManager mAccountManager;

    private final Object mServiceLock = new Object();
    private IKeyChainService mService;
    private boolean mIsBound;

    private Account mAccount;

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override public void onServiceConnected(ComponentName name, IBinder service) {
            synchronized (mServiceLock) {
                mService = IKeyChainService.Stub.asInterface(service);
                mServiceLock.notifyAll();

                // Account is created if necessary during binding of the IKeyChainService
                mAccount = mAccountManager.getAccountsByType(ACCOUNT_TYPE)[0];
            }
        }

        @Override public void onServiceDisconnected(ComponentName name) {
            synchronized (mServiceLock) {
                mService = null;
            }
        }
    };

    private final Context mContext;

    private final CloseGuard mGuard = CloseGuard.get();

    private KeyChain(Context context) throws InterruptedException {
        if (context == null) {
            throw new NullPointerException("context == null");
        }
        mContext = context;
        ensureNotOnMainThread();
        mAccountManager = AccountManager.get(mContext);
        mIsBound = mContext.bindService(new Intent(IKeyChainService.class.getName()),
                                        mServiceConnection,
                                        Context.BIND_AUTO_CREATE);
        if (!mIsBound) {
            throw new AssertionError();
        }
        synchronized (mServiceLock) {
            // there is a race between binding on this thread and the
            // callback on the main thread. wait until binding is done
            // to be sure we have the mAccount initialized.
            if (mService == null) {
                mServiceLock.wait();
            }
        }
        mGuard.open("close");
    }

    /**
     * {@code Bundle} will contain {@link #KEY_INTENT} if user needs
     * to confirm application access to requested key. In the alias
     * does not exist or there is an error, null is
     * returned. Otherwise the {@code Bundle} contains information
     * representing the private key which can be interpreted with
     * {@link #toPrivateKey toPrivateKey}.
     *
     * non-null alias
     */
    public Bundle getPrivate(String alias) {
        return get(alias, Credentials.USER_PRIVATE_KEY);
    }

    public Bundle getCertificate(String alias) {
        return get(alias, Credentials.USER_CERTIFICATE);
    }

    public Bundle getCaCertificate(String alias) {
        return get(alias, Credentials.CA_CERTIFICATE);
    }

    private Bundle get(String alias, String type) {
        if (alias == null) {
            throw new NullPointerException("alias == null");
        }
        ensureNotOnMainThread();

        String authAlias = (type.equals(Credentials.CA_CERTIFICATE)) ? (alias + CA_SUFFIX) : alias;
        AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(mAccount,
                                                                           authAlias,
                                                                           false,
                                                                           null,
                                                                           null);
        Bundle bundle;
        try {
            bundle = future.getResult();
        } catch (OperationCanceledException e) {
            throw new AssertionError(e);
        } catch (IOException e) {
            throw new AssertionError(e);
        } catch (AuthenticatorException e) {
            throw new AssertionError(e);
        }
        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
        if (intent != null) {
            Bundle result = new Bundle();
            // we don't want this Eclair compatability flag,
            // it will prevent onActivityResult from being called
            intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
            result.putParcelable(KEY_INTENT, intent);
            return result;
        }
        String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        if (authToken == null) {
            throw new AssertionError("Invalid authtoken");
        }

        byte[] bytes;
        try {
            if (type.equals(Credentials.USER_PRIVATE_KEY)) {
                bytes = mService.getPrivate(alias, authToken);
            } else if (type.equals(Credentials.USER_CERTIFICATE)) {
                bytes = mService.getCertificate(alias, authToken);
            } else if (type.equals(Credentials.CA_CERTIFICATE)) {
                bytes = mService.getCaCertificate(alias, authToken);
            } else {
                throw new AssertionError();
            }
        } catch (RemoteException e) {
            throw new AssertionError(e);
        }
        if (bytes == null) {
            throw new AssertionError();
        }
        Bundle result = new Bundle();
        if (type.equals(Credentials.USER_PRIVATE_KEY)) {
            result.putByteArray(KEY_PKCS8, bytes);
        } else if (type.equals(Credentials.USER_CERTIFICATE)) {
            result.putByteArray(KEY_X509, bytes);
        } else if (type.equals(Credentials.CA_CERTIFICATE)) {
            result.putByteArray(KEY_X509, bytes);
        } else {
            throw new AssertionError();
        }
        return result;
    }

    public static PrivateKey toPrivateKey(Bundle bundle) {
        byte[] bytes = bundle.getByteArray(KEY_PKCS8);
        if (bytes == null) {
            throw new IllegalArgumentException("not a private key bundle");
        }
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes));
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        } catch (InvalidKeySpecException e) {
            throw new AssertionError(e);
        }
    }

    public static Bundle fromPrivateKey(PrivateKey privateKey) {
        Bundle bundle = new Bundle();
        String format = privateKey.getFormat();
        if (!format.equals("PKCS#8")) {
            throw new IllegalArgumentException("Unsupported private key format " + format);
        }
        bundle.putByteArray(KEY_PKCS8, privateKey.getEncoded());
        return bundle;
    }

    public static X509Certificate toCertificate(Bundle bundle) {
        byte[] bytes = bundle.getByteArray(KEY_X509);
        if (bytes == null) {
            throw new IllegalArgumentException("not a certificate bundle");
        }
        try {
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
            return (X509Certificate) cert;
        } catch (CertificateException e) {
            throw new AssertionError(e);
        }
    }

    public static Bundle fromCertificate(Certificate cert) {
        Bundle bundle = new Bundle();
        String type = cert.getType();
        if (!type.equals("X.509")) {
            throw new IllegalArgumentException("Unsupported certificate type " + type);
        }
        try {
            bundle.putByteArray(KEY_X509, cert.getEncoded());
        } catch (CertificateEncodingException e) {
            throw new AssertionError(e);
        }
        return bundle;
    }

    private void ensureNotOnMainThread() {
        Looper looper = Looper.myLooper();
        if (looper != null && looper == mContext.getMainLooper()) {
            throw new IllegalStateException(
                    "calling this from your main thread can lead to deadlock");
        }
    }

    public Bundle findIssuer(X509Certificate cert) {
        if (cert == null) {
            throw new NullPointerException("cert == null");
        }
        ensureNotOnMainThread();

        // check and see if the issuer is already known to the default IndexedPKIXParameters
        IndexedPKIXParameters index = SSLParametersImpl.getDefaultIndexedPKIXParameters();
        try {
            TrustAnchor anchor = index.findTrustAnchor(cert);
            if (anchor != null && anchor.getTrustedCert() != null) {
                X509Certificate ca = anchor.getTrustedCert();
                return fromCertificate(ca);
            }
        } catch (CertPathValidatorException ignored) {
        }

        // otherwise, it might be a user installed CA in the keystore
        String alias;
        try {
            alias = mService.findIssuer(fromCertificate(cert));
        } catch (RemoteException e) {
            throw new AssertionError(e);
        }
        if (alias == null) {
            Log.w(TAG, "Lookup failed for issuer");
            return null;
        }

        Bundle bundle = get(alias, Credentials.CA_CERTIFICATE);
        Intent intent = bundle.getParcelable(KEY_INTENT);
        if (intent != null) {
            // permission still required
            return bundle;
        }
        // add the found CA to the index for next time
        X509Certificate ca = toCertificate(bundle);
        index.index(new TrustAnchor(ca, null));
        return bundle;
    }

    public void close() {
        if (mIsBound) {
            mContext.unbindService(mServiceConnection);
            mIsBound = false;
            mGuard.close();
        }
    }

    protected void finalize() throws Throwable {
        // note we don't close, we just warn.
        // shouldn't be doing I/O in a finalizer,
        // which the unbind would cause.
        try {
            if (mGuard != null) {
                mGuard.warnIfOpen();
            }
        } finally {
            super.finalize();
        }
    }
}